通俗易懂如何设计能支撑百万并发的数据库架构

1、引言相信看到这个标题,很多人的第一反应就是:对数据库进行分库分表啊!但是实际上,数据库层面的分库分表到底是用来干什么的,其不同的作用如何应对不同的场景,我觉得很多同学可能都没搞清楚。 本篇文章我们一起来学习一下,对于一个支撑日活百万用户的高并发系统,数据库架构应该如何设计呢? 本文的讨论和分享,将用一个创业公司的发展作为背景引入,方便大家理解。 (本文同步发布于:http://www.52im.net/thread-25...) 2、相关文章高性能数据库方面的文章: 《优秀后端架构师必会知识:史上最全MySQL大表优化方案总结》《阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史》 《阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路》 《腾讯TEG团队原创:基于MySQL的分布式数据库TDSQL十年锻造经验分享》 分布式架构方面的入门文章: 《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》 《快速理解高性能HTTP服务端的负载均衡技术原理》 《一篇读懂分布式架构下的负载均衡技术:分类、原理、算法、常见方案等》 3、小型系统的典型数据库单机架构和明显的瓶颈假如我们现在是一个小创业公司,注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10。 天呐!就这种系统,随便找一个有几年工作经验的高级工程师,然后带几个年轻工程师,随便干干都可以做出来。 因为这样的系统,实际上主要就是在前期进行快速的业务功能开发,搞一个单块系统部署在一台服务器上,然后连接一个数据库就可以了。 接着大家就是不停地在一个工程里填充进去各种业务代码,尽快把公司的业务支撑起来。 如下图所示: 结果呢,没想到我们运气这么好,碰上个优秀的 CEO 带着我们走上了康庄大道! 公司业务发展迅猛,过了几个月,注册用户数达到了 2000 万!每天活跃用户数 100 万!每天单表新增数据量达到 50 万条!高峰期每秒请求量达到 1 万! 同时公司还顺带着融资了两轮,估值达到了惊人的几亿美金!一只朝气蓬勃的幼年独角兽的节奏! 好吧,现在大家感觉压力已经有点大了,为啥呢?因为每天单表新增 50 万条数据,一个月就多 1500 万条数据,一年下来单表会达到上亿条数据。 经过一段时间的运行,现在咱们单表已经两三千万条数据了,勉强还能支撑着。 但是,眼见着系统访问数据库的性能怎么越来越差呢,单表数据量越来越大,拖垮了一些复杂查询 SQL 的性能啊! 然后高峰期请求现在是每秒 1 万,咱们的系统在线上部署了 20 台机器,平均每台机器每秒支撑 500 请求,这个还能抗住,没啥大问题。但是数据库层面呢? 如果说此时你还是一台数据库服务器在支撑每秒上万的请求,负责任的告诉你,每次高峰期会出现下述问题: 1)你的数据库服务器的磁盘 IO、网络带宽、CPU 负载、内存消耗,都会达到非常高的情况,数据库所在服务器的整体负载会非常重,甚至都快不堪重负了; 2)高峰期时,本来你单表数据量就很大,SQL 性能就不太好,这时加上你的数据库服务器负载太高导致性能下降,就会发现你的 SQL 性能更差了; 3)最明显的一个感觉,就是你的系统在高峰期各个功能都运行的很慢,用户体验很差,点一个按钮可能要几十秒才出来结果; 4)如果你运气不太好,数据库服务器的配置不是特别的高的话,弄不好你还会经历数据库宕机的情况,因为负载太高对数据库压力太大了。 4、多台服务器分库支撑高并发读写首先我们先考虑第一个问题,数据库每秒上万的并发请求应该如何来支撑呢? 要搞清楚这个问题,先得明白一般数据库部署在什么配置的服务器上。通常来说,假如你用普通配置的服务器来部署数据库,那也起码是 16 核 32G 的机器配置。 ...

May 15, 2019 · 5 min · jiezi

Apache-Cassandra-数据存储模型

我们在《Apache Cassandra 简介》文章中介绍了 Cassandra 的数据模型类似于 Google 的 Bigtable,对应的开源实现为 Apache HBase,而且我们在 《HBase基本知识介绍及典型案例分析》 文章中简单介绍了 Apache HBase 的数据模型。按照这个思路,Apache Cassandra 的数据模型应该和 Apache HBase 的数据模型很类似,那么这两者的数据存储模型是不是一样的呢?本文将为大家解答这些问题。我们从 KeySpace -> Table -> Partition -> Row -> Cell 顺序介绍。本文基于 Apache Cassandra 3.11.4 源码进行介绍的,不同版本可能有些不一样。 Table & KeySpaceCassandra 中的 KeySpace 概念和 RDBMS 里面的 DataBase 概念很类似,一个 KeySpace 包含多张表,一般将有关联的数据表放到同一个 KeySpace 下面。KeySpace 创建的时候可以指定副本策略,副本因子以及是否启用 CommitLog 机制(类似 HBase 中的 WAL)。 Cassandra 中表的概念和 RDBMS 很类似。不同的是在 Cassandra 中属于同一张表的数据在物理上是分布在不同节点上存储的,同一张表由多个 Partition 组成。 PartitionsCassandra 一般是由多台节点组成的,每台节点负责一定范围的,如果使用 Murmur3hash 的时候,每个节点负责的 Token 类似于下面那样: ...

May 10, 2019 · 3 min · jiezi

Apache-Cassandra-static-column-介绍与实战

假设我们有这样的场景:我们想在 Cassandra 中使用一张表记录用户基本信息(比如 email、密码等)以及用户状态更新。我们知道,用户的基本信息一般很少会变动,但是状态会经常变化,如果每次状态更新都把用户基本信息都加进去,势必会让费大量的存储空间。为了解决这种问题,Cassandra 引入了 static column。同一个 partition key 中被声明为 static 的列只有一个值的,也就是只存储一份。 定义 static column在表中将某个列定义为 STATIC 很简单,只需要在列的最后面加上 STATIC 关键字,具体如下: CREATE TABLE "iteblog_users_with_status_updates" ( "username" text, "id" timeuuid, "email" text STATIC, "encrypted_password" blob STATIC, "body" text, PRIMARY KEY ("username", "id"));iteblog_users_with_status_updates 表中我们将 email 和 encrypted_password 两个字段设置为 STATIC 了,这意味着同一个 username 只会有一个 email 和 encrypted_password 。 注意,不是任何表都支持给列加上 STATIC 关键字的,静态列有以下限制。 1、如果表没有定义 Clustering columns(又称 Clustering key),这种情况是不能添加静态列的。如下: cqlsh:iteblog_keyspace> CREATE TABLE "iteblog_users_with_status_updates_invalid" ( ... "username" text, ... "id" timeuuid, ... "email" text STATIC, ... "encrypted_password" blob STATIC, ... "body" text, ... PRIMARY KEY ("username") ... );InvalidRequest: Error from server: code=2200 [Invalid query] message="Static columns are only useful (and thus allowed) if the table has at least one clustering column"iteblog_users_with_status_updates_invalid 表只有 PRIMARY KEY,没有定义 clustering column,不支持创建 Static columns。这是因为静态列在同一个 partition key 存在多行的情况下才能达到最优情况,而且行数越多效果也好。但是如果没有定义 clustering column,相同 PRIMARY KEY 的数据在同一个分区里面只存在一行数据,本质上就是静态的,所以没必要支持静态列。 ...

April 30, 2019 · 3 min · jiezi

高并发架构的CDN知识介绍

对一次网络请求过程的了解程度,一是展现你的专业知识;二是深刻的理解,让你在大型网站架构中做出更适合、可靠的架构。而DNS是这一切的出发点,本文结合一张常用架构图,来描述一下这个过程。 部署架构大型的web服务,我们的部署架构一般如下图。先上图再解释。 这里来解释下,为什么要这样架构。首先客户端的请求会通过 DNS 获取到对应的服务器IP(实际上是LB的ip地址),这一层会有 DNS的负载均衡,并且如果是静态站资源会进入到CDN,这里DNS与CDN如何完成接棒的过程,后面会详细解释。当请求到达LB层的时候(应用层协议是HTTP协议),这一层又会做一次负载均衡(可能用LVS或者Nginx做)。这里我们有两种不同的处理方式,一条路径会进入到代理集群,一条路径直接进入到应用集群。这是为什么? LB到代理集群通过最顶层的LB负责均衡后到达代理机器,这里不直接进入到应用集群,还要搞一层代理的目的主要是方便我们在代理集群进行各种高级(骚)操作。 比如:请求日志收集,自定义缓存,自定义的负载均衡,自定义的路由规则制定(跨机房,路由分组) LB到应用集群上面到代理层有那么多好处,为什么还有绕过代理层这条路径存在呢?这主要是针对大流量服务。因为代理层因为有很多额外的操作,导致响应会变长,路径增加,到下一个集群多了一次网络传输往返。 所以,一般针对大流量服务,为了防止代理被打满,响应更快,会直接在外网LB上进行负载到应用集群。 通过上面的分割后,最终都会到达应用集群,每一台机器上我们会部署一台 Nginx 来按照域名转到对应服务,当然这里完全也可以不是 Nginx ,比如微服务,这里可能是一个 SideCard 代理。这里主要是为了便于说明我们后面全部都是当成Nginx。服务调用 DB Cache 等,都是通过域名,这是为了负载均衡,请求时,会通过内网DNS服务,完成域名解析,然后拿到内网的 LB 的IP。然后再这里进行内网的负载均衡,会根据域名的端口来检查你是写操作、还是读操作返回IP。常规一点会保证是单点写入,多点读取。来完成数据一致性的保障。 整个大体过程如此,接下来我们详细说一下 DNS 与 CDN 相关的工作原理。 DNS如何实现IP查找为了后面说清楚CDN,这里先介绍DNS的解析过程。当然此类文章网络上已经极多。但是我还是想按照我的理解来说一下DNS是如何工作的。 在整个DNS过程中有四个重要概念,下面解释下。 DNS Resolver - 递归解析器,主要是接收客户端发出的域名解析请求,并发送 DNS query 查询请求。对于客户端来说它不需要任何操劳,等待 DNS Resolver 告诉自己域名转IP的结果就好。 Root Server - 这是转换IP执行的第一步查询,根服务器并不会保存具体的域名IP映射信息。它就像一个索引服务器,会告诉你下一步该去那台 TLD Server 查询。 TLD Server - 这是顶级域名服务器,是执行IP查询的第二步,这里会告诉 DNS Resolver 权威域名服务器的地址。 Authoriative Server - 权威域名服务器就是包含了完整的机器名的域名,例如:www.example.com ,在这台机器上保存了这个具体域名对应的IP地址。 下面根据图中的十个步骤说一下每一步都在干嘛。 一个用户在浏览器输入了:example.com,这时会产生一个 DNS 查询,从而进入到 DNS Resolver中;Resolver 会进入到 root server 进行查询;root server 返回了 TLD server 的地址,查询请求转向顶级域名服务,这里是 .com 服务器。递归解析器向 .com 服务器发送一个请求;TLD server 收到请求后会返回 example.com 权威服务器的地址;递归解析器又发了一个向权威服务器查询的请求,至此权威服务器查询自己的映射表拿到IP;返回查询到的IP给了 DNS Resolver;DNS Resolver返回IP给浏览器,浏览器将会用这个IP来建立连接,发起请求;客户端通过这个IP地址,发起一个 HTTP 请求;服务器解析请求,并返回数据到浏览器。这里需要补充一点是,上面每一步其实都有DNS缓存的设计。比如: ...

April 30, 2019 · 1 min · jiezi

订单状态一致性维护的高鲁棒性实践以接单系统为例

订单系统最难的就是一致性维护,关于状态机,同事有很好的总结,参见:https://www.jianshu.com/c/fcf... 接单系统作为订单系统的后路,对一致性要求更高 接单系统特点与要求 TPS高,要求高并发,存在峰值流量稳定性要求高,不能漏接单,需要有较高的容错性,接单系统依赖的任何服务短时挂掉都不应该影响接单,尤其是在电商系统中,到接单这一步,用户之前其实已经支付了,所以对稳定性有着异常严格的要求。技术实现 1.对于高TPS要求,实现的套路是固定的。 消息中间件在工程实现中主要用来削峰,异步和解耦。接单接口的入口首先要用消息队列来削峰,以解决峰值高流量问题。 削峰之后收到要保证消息不堆积并支持高并发,还需要做分流,接单系统需要落日志并增删改查业务表数据,数据库性能就会成为瓶颈,数据库的分库分表就是必须的, 消息加数据库分库分表基本就能满足。 2.对于稳定性要求 需要考虑到各种异常情况。出现异常情况要有自恢复能力,状态机加延迟队列再加脚本可解决问题。 为什么要有状态机呢,接单系统的业务逻辑一般都很复杂,在接单的过程中可能由于各种原因导致异常,这时候状态机的作用就凸显, 刚接到单,就需要数据落库,这是状态机的初始态。之后的业务逻辑流转过程中的各种可能异常情况都需要被记录下来,这些是状态机的中间态,属于异常情况, 需要某种机制将中间态转化为最终态。技术要需要利用延迟队列,如果某个订单出现了异常,或者说出现了中间态,需要将这个异常订单扔到延迟队列中,待会重试, 绝大多数情况是依赖的某个微服务短时间存在不可用或者不稳定,待它稳定后需要进行重试。重试如果还有问题就再重试。 但是重试也需要有次数限制,如果有问题,不可能一直在堆在q中,这时候补偿脚本的作用就显现了 ,脚本是最后的兜底,跟常规逻辑相比,脚本需要对业务稍微有损,是次优的, 在兜底脚本中可将部分接单过程中不重要的接口或者功能降级掉,优先保证接单的稳定。这是跟之前的延迟队列的业务逻辑相比不同的地方。

April 26, 2019 · 1 min · jiezi

大促系统实践经验

电商营销中少不了大促,根据长期的工作经验,总结了大促系统核心需要关注以下几个点:1.隔离绝大多数公司的大促类需求都是下单链路的旁路,就是说要做到大促失败是不影响下单的,这就要求大促系统做到隔离,隔离主要指的是中间件及机器的隔离,对于中间件,数据库,q缓存,都不应该跟其他人混合部署,个人之前是吃过大亏的,还有机器,现在很多都是虚拟机,但是隔离做的很差,如果跟下单链路公用宿主机,就有可能造成下单抖动2.sharding 理论上来讲系统的qps是没有上限的,只要是分布式的架构.在大促中系统中q和数据库一定要shardingq在在大促是必不可少的,用来削峰,异步和解耦,在异步场景中流量是首先到q的,如果做不好shardingq的消息堆积能力就会大打折扣;还有一个是数据库,最脆弱的就是数据库,但是如果数据库分库分表做的好数据库就不会成为瓶颈3.限流限流技术现在已经有通用的解决方案,比如阿里基于Sentinel的限流及一些基于网关层的限流工程中主要是这样用的,理论上主要分为计数法和桶法 计数法包括固定计数法和滑动窗口计数法桶发放主要是漏桶算法和令牌桶算法,漏桶算法可以简单理解为是生产者/消费者模式在流量上的应用,简单高效令牌桶算法生产者以恒定的速度生产令牌放到令牌桶中(决定了QPS阈值)每个请求去获取令牌,获取到就执行,获取不到则触发限流4.错峰在削峰填谷的思路下任何形式的错峰都是有必要的,前端random也是一种思路,打撒后把流量放到后端,在业务层面能错峰的思路更多,比如玩游戏,每个人有不同的游戏时间等 5.奥卡姆剃刀原理要对自己的大促场景有清晰的认知,遵循奥卡姆剃刀原理,就是简单就是有效原理,举个例子,大促场景下是不是所有的扣减库存都要在redis等中间件中扣除,很多是没有必要的,只要不是单点问题或者热点数据,不存在短时频繁更新数据库的场景,在DB操作完全够了。比如电商店铺维度的抢购数据库扣减库存就够了,天猫也是这么处理的。但是如果是海量用户抢购一个商品这种场景,一定要用redis等中间件,扣减之前还需要做预热

April 26, 2019 · 1 min · jiezi

贝壳金服 TiDB 在线跨机房迁移实践

作者介绍 :李振环,贝壳金服数据基础架构负责人,目前负责数据平台和企业级数据仓库开发。公司介绍贝壳金服总部位于北京,起步于 2006 年成立的链家金融。2017 年 5 月,贝壳正式独立运作,是国内领先的居住金融服务商。在租赁、家装、买卖、安居这四个居住的典型消费场景中为用户提供支付、贷款等定制化的消费金融服务。意旨通过“产品”、“技术”和“服务”让客户的美好生活来的更早一些。仅房屋交易一项,贝壳金服已经为 100 万用户提供过贝壳安心服务,其中,支付服务中的资金流转量达到 3750 亿元。贝壳金服目前全国有 1500 多名产品技术人员与金融顾问,覆盖中国 28 个城市及地区,以独家大数据与场景风控能力见长。 项目背景贝壳金服数据中台使用 TiDB 和 TiSpark 平台,基于 Syncer 将业务数据实时从 MySQL 备库抽取到 TiDB 中,并通过 TiSpark 对 TiDB 中的数据进行数据分析处理,供上游业务消费,现已服务于 70 多名数据开发人员。现有集群已经使用 100 多个 Syncer 同步上游 MySQL 数据,目前已经达到 4.7TB 热数据,上百张离线和实时报表。由于机房调整,数据中台也需要同步迁移到新机房,结合 TiDB 的特性,我们探索了一种在线不停机迁移机房的方式。 TiDB 是一个分布式 NewSQL 数据库。它支持水平弹性扩展、ACID 事务、MySQL 语法,具有数据强一致的高可用特性,是一个不仅适合 OLTP 场景还适合 OLAP 场景的混合数据库。而 TiSpark 是为解决较重的 OLAP 需求而推出的产品。它借助 Spark 平台,同时融合 TiKV 分布式集群的优势,和 TiDB 一起为用户一站式解决 HTAP 的业务需求。TiSpark 依赖于 TiKV 集群和 PD 组件,使用同一个数据源,减少对于 ETL 工具的维护,并且可以使用 Spark 进行复杂查询计算。 ...

April 23, 2019 · 2 min · jiezi

总要先爬出坑的JEE架构

本博客 猫叔的博客,转载请申明出处先来看看官网对它的定义。Java平台企业版(Java EE)是社区驱动的企业软件的标准。Java EE是使用Java Community Process开发的,其中包括来自行业专家,商业和开源组织,Java用户组以及无数个人的贡献。每个版本都集成了符合行业需求的新功能,提高了应用程序的可移植性并提高了开发人员的工作效率 如今,Java EE的提供了丰富的企业软件平台,并与超过 20个兼容的Java EE实现可供选择。Java EE 8,你值得了解,起码官网还提示了你它还在更新新的功能。说到JEE,做web项目的朋友其实都有所了解,它将企业级软件架构分为三个层级,web层、业务逻辑层和数据存储层。先看看图,旧时代的辉煌!先介绍一下:WEB容器:给处于其中的应用程序组件(JSP,SERVLET)提供一个环境,使JSP,SERVLET直接跟容器中的环境变量接口交互,不必关注其它系统问题。主要由WEB服务器来实现。例如:TOMCAT,WEBLOGIC,WEBSPHERE等。该容器提供的接口严格遵守J2EE规范中的WEB APPLICATION 标准。我们把遵守以上标准的WEB服务器就叫做J2EE中的WEB容器。同时,JEE 平台将不同的模块化组件聚合后运行在通用的应用服务器上,例WebLogi,WebSphere , JBoss 等,这也包含 Tomcat Tomcat 仅仅是实现了 JEE Web 规范的 Web 容器。EJB容器:Enterprise java bean 容器。更具有行业领域特色。他提供给运行在其中的组件EJB各种管理功能。只要满足J2EE规范的EJB放入该容器,马上就会被容器进行高效率的管理。并且可以通过现成的接口来获得系统级别的服务。例如邮件服务、事务管理。WEB容器和EJB容器在原理上是大体相同的,更多的区别是被隔离的外界环境。WEB容器更多的是跟基于HTTP的请求打交道。而EJB容器不是。它是更多的跟数据库、其它服务打交道。但他们都是把与外界的交互实现从而减轻应用程序的负担。例如SERVLET不用关心HTTP的细节,直接引用环境变量session,request,response就行、EJB不用关心数据库连接速度、各种事务控制,直接由容器来完成。可以看到每个层次的职责如下:Web层:负责与用户交互或者对外提供接口业务逻辑层:为了实现业务逻辑而设计的流程处理和计算处理模块数据存取层:将业务逻辑层处理的结果持久化以待后续查询,并维护领域模型中对象的生命周期。值得一提的是,JEE平台是典型的二八原则的应用场景,它将 80%通用的与业务无关的逻辑和流程封装在应用服务器的模块化组件里,通过配置的模式提供给应用程序访问,应用程序实现 20%专用逻辑,并通过配置的形式来访问应用服务器提供的模块化组件。事实上,应用服务器提供的对象关系映射服务、数据持久服务、事务服务、安全服务、消息服务等通过简单的配置即可在应用程序中使用。JEE 时代的架构已经对企业级应用的整体架构进行了逻辑分层,包括上面提到的 Web 层、业务逻 和数据存取层,分别对应上图中的 Web 容器、 JB 容器和数据存取 ORM 组件与数据持久层 (数据库) 不同的层级有自己的职责,并从功能类型上划分层级,每个层级的职责单一。在分层架构下需要对项目管理过程中的团队进行职责划分,井建立团队交流机制。根据康威定律,设计系统的组织时,最终产生的设计等价于组织的沟通结构 ,通俗来讲,团队的交流机制应该与架构分层交互机制相对应。由于在架构上把整体的单体系统分成具有不同职责的层级,对应的项目管理倾向于把大的团队分成不同的职能团队,主要包括:用户 交互 UI 团队、后台业务逻辑处理团队、 数据存取 ORM 团队与 DBA 团队等,每个团队只对自己的职责负责,并对使用方提供组件服务质量保证。让我们在看看另一个经典,职能团队划分。JEE通过对单体架构的分层,结合职能划分,开始通过架构在一定程度上进行逻辑拆分,让各个专业的人能更加高效的做他们应该做的事情。但是,每个层次的多个业务逻辑的实现会被放在同一应用项目中,并且运行在同一个服务器上。尽管大多数公司会使用规范来约束不同业务逻辑的隔离性来解祸,但是久而久之,随着复杂业务逻辑的选代增加及开发人员的不断流动,新的程序员为了节省时间和赶进度,非法使用了其他组件的服务,业务组件之间、 组件之间、数据存取之间的稿合性必然增加,最后导致组件与组件之间难以划清界限,完全祸合在一起,将来的新功能迭代、增加和维护将难上加难。(反正你如果是入职接手一个老项目,那你一般都会很头疼)就当时而言,尽管 JEE 支持 Web容器和 EJB 容器的分离部署,大多数项目仍然部署在同 个应用服务器上井跑在一JVM 进程中。说说你和JEE的那些事吧!公众号:Java猫说学习交流群:728698035现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

April 19, 2019 · 1 min · jiezi

华为云服务认证首席架构师邀你免费学习课程,参加限时活动领音响!

华为云服务认证首席架构师邀你免费学习课程,参加限时活动领音响!http://t.cn/EXnMeDo

April 18, 2019 · 1 min · jiezi

余额,危险的操作,给996留点福报

真的很危险,有人因此进了局子;也有公司因此损失上亿。想象一下你在一个月黑风高的夜晚,大概是10点多钟的样子,加班归来,打算到小卖部弄盒烟抽。夜凉风急,你用力裹了下被风鼓起的外套。那里有你暗恋的收银姑娘。没日没夜的工作,只有这十几分钟,能让你感到些许生活的意义。从羞涩的钱包里翻出仅存的一张百元大钞,结账。然后用颤抖的双手接过收银员的找零。不是因为轻触到了她的指尖。也并非因她如花的笑靥。只因为,脑海里竟然不争气的浮现出这样的过程。balance = dao.getBalance(userid)balance = balance - costdao.setBalance(userid,balance)还真是狗改不了吃屎啊,果然还是一个码畜。提醒着自己,自卑的埋下了脸,快步走开。这是什么?这是我们送给996公司的一点福报。一波麻6的操作余额修改,是交易系统里最常见的操作。上面的伪代码,大意是先取出余额,然后扣掉消费,然后再回写余额。通常情况下这不会发生问题。除非是高并发,与你是否单机无关。对单一余额的高并发操作自然不是正常人发起的,系统正在承受攻击,或者自以为是的使用了MQ。在攻击面前,上面的操作显得不堪一击。拿一个最严重的例子说明:同时发起了一笔消费20元和消费5元的请求。在经过一波猛如虎的操作之后,两个请求都支付成功了,但只扣除了5元。相当于花了5块钱,买了25的东西。划重点:把以上操作扩展到提现操作上,就更加的恐怖。比如你发起了一笔100元的提现和0.01元的提现,结果余额被扣减0.01元的提现给覆盖了。这相当于你有了一个提款机,非要薅到平台倒闭为止。防护办法通过SQL解决update user set balance = balance - 20 where userid=id这条语句就保险了很多,如果考虑到余额不能为负的情况,可以把sql更加精进一点。update user set balance = balance - 20 where userid=id and balance >= 20以上sql,就可以保证余额的安全,高并发下的攻击就变得意义不大了。但会有别的问题,比如重复扣款。通过锁解决现实中,这种直接通过sql扣减的应用,规模都比较小。当你的业务逐渐复杂,又没有进行很好的拆分的情况下,先读再设值的情况还是比较普遍的。比如某些营销操作、打折、积分兑换等。这种情况,可以引入分布式锁。简单点的,只需要使用redis的setnx或者zk来控制就可以;复杂点的方案,可以使用二阶段提交之类的。分布式事务的业务粒度,要足够粗,才能保护这些余额操作;加锁的粒度,要足够细,才能保证系统的效率。begin transition(userid) balance = dao.getBalance(userid) balance = balance - cost dao.setBalance(userid,balance)end类CAS方式解决java的朋友可以回想下concurrent包的解决方式。那就是引入了CAS,全称Compare And Set。扩展到分布式环境下,同样可以采用这一策略。即先比较再设值。如果初始值已经变化了,那么不允许set设值。cas一般通过循环重试的方法进行状态更新,但余额操作一般都是比较单一的,你也可以直接终止操作,并预警风险。sql类似于:update user set balance = balance - 20 where userid=id and balance >= 20 and balance = $old_balance当然,你也可以通过加入版本号概念,而不是余额字段来控制这个过程,但都类似。变种:版本号通过在表中加一个额外的字段version,来控制并发。这种方式不去关注余额,可扩展性更强。version的默认值一般是1,即记录创建时的默认值。操作的伪代码如下:version,balance = dao.getBalance(userid)balance = balance - costdao.exec(" update user set balance = balance - 20 version = version + 1 where userid=id and balance >= 20 and version = $old_version")上面的并发攻击,将会只有一个操作能够成功,我们的余额安全了。End赶紧看一下你的余额操作,是否也暴露在风险之下。你可以选择接受福报继续当兄弟,当然也可以将福报还给资本家。一念成佛,一念成魔。你才是自己的主人。 ...

April 16, 2019 · 1 min · jiezi

有赞百亿级日志系统架构设计

一、概述日志是记录系统中各种问题信息的关键,也是一种常见的海量数据。日志平台为集团所有业务系统提供日志采集、消费、分析、存储、索引和查询的一站式日志服务。主要为了解决日志分散不方便查看、日志搜索操作复杂且效率低、业务异常无法及时发现等等问题。随着有赞业务的发展与增长,每天都会产生百亿级别的日志量(据统计,平均每秒产生 50 万条日志,峰值每秒可达 80 万条)。日志平台也随着业务的不断发展经历了多次改变和升级。本文跟大家分享有赞在当前日志系统的建设、演进以及优化的经历,这里先抛砖引玉,欢迎大家一起交流讨论。二、原有日志系统有赞从 16 年就开始构建适用于业务系统的统一日志平台,负责收集所有系统日志和业务日志,转化为流式数据,通过 flume 或者 logstash 上传到日志中心(kafka 集群),然后共 Track、Storm、Spark 及其它系统实时分析处理日志,并将日志持久化存储到 HDFS 供离线数据分析处理,或写入 ElasticSearch 提供数据查询。整体架构如下图 所示。随着接入的应用的越来越多,接入的日志量越来越大,逐渐出现一些问题和新的需求,主要在以下几个方面:1.业务日志没有统一的规范,业务日志格式各式各样,新应用接入无疑大大的增加了日志的分析、检索成本。2.多种数据日志数据采集方式,运维成本较高3.存储方面,-采用了 Es 默认的管理策略,所有的 index 对应 3*2个shard(3 个 primary,3 个 replica),有部分 index 数量较大,对应单个 shard 对应的数据量就会很大,导致有 hot node,出现很多 bulk request rejected,同时磁盘 IO 集中在少数机器上。-对于 bulk request rejected 的日志没有处理,导致业务日志丢失-日志默认保留 7 天,对于 ssd 作为存储介质,随着业务增长,存储成本过于高昂-另外 Elasticsearch 集群也没有做物理隔离,Es 集群 oom 的情况下,使得集群内全部索引都无法正常工作,不能为核心业务运行保驾护航4.日志平台收集了大量用户日志信息,当时无法直接的看到某个时间段,哪些错误信息较多,增加定位问题的难度。三、现有系统演进日志从产生到检索,主要经历以下几个阶段:采集->传输->缓冲->处理->存储->检索,详细架构如下图所示:3.1日志接入日志接入目前分为两种方式,SDK 接入和调用 Http Web 服务接入-SDK 接入:日志系统提供了不同语言的 SDK,SDK 会自动将日志的内容按照统一的协议格式封装成最终的消息体,并最后最终通过 TCP 的方式发送到日志转发层(rsyslog-hub)-Http Web 服务接入:有些无法使用 SDk 接入日志的业务,可以通过 Http 请求直接发送到日志系统部署的 Web 服务,统一由 web protal 转发到日志缓冲层的 kafka 集群3.2日志采集现在有 rsyslog-hub 和 web portal 做为日志传输系统,rsyslog 是一个快速处理收集系统日志的程序,提供了高性能、安全功能和模块化设计。之前系统演进过程中使用过直接在宿主机上部署 flume 的方式,由于 flume 本身是 java 开发的,会比较占用机器资源而统一升级为使用 rsyslog 服务。为了防止本地部署与 kafka 客户端连接数过多,本机上的 rsyslog 接收到数据后,不做过多的处理就直接将数据转发到 rsyslog-hub 集群,通过 LVS 做负载均衡,后端的 rsyslog-hub 会通过解析日志的内容,提取出需要发往后端的 kafka topic。3.3日志缓冲Kafka 是一个高性能、高可用、易扩展的分布式日志系统,可以将整个数据处理流程解耦,将 kafka 集群作为日志平台的缓冲层,可以为后面的分布式日志消费服务提供异步解耦、削峰填谷的能力,也同时具备了海量数据堆积、高吞吐读写的特性。3.4日志切分日志分析是重中之重,为了能够更加快速、简单、精确地处理数据。日志平台使用 spark streaming 流计算框架消费写入 kafka 的业务日志,Yarn 作为计算资源分配管理的容器,会跟不同业务的日志量级,分配不同的资源处理不同日志模型。整个 spark 任务正式运行起来后,单个批次的任务会将拉取的到所有的日志分别异步的写入到 ES 集群。业务接入之前可以在管理台对不同的日志模型设置任意的过滤匹配的告警规则,spark 任务每个 excutor 会在本地内存里保存一份这样的规则,在规则设定的时间内,计数达到告警规则所配置的阈值后,通过指定的渠道给指定用户发送告警,以便及时发现问题。当流量突然增加,es 会有 bulk request rejected 的日志会重新写入 kakfa,等待补偿。3.5日志存储-原先所有的日志都会写到 SSD 盘的 ES 集群,logIndex 直接对应 ES 里面的索引结构,随着业务增长,为了解决 Es 磁盘使用率单机最高达到 70%80% 的问题,现有系统采用 Hbase 存储原始日志数据和 ElasticSearch 索引内容相结合的方式,完成存储和索引。-Index 按天的维度创建,提前创建index会根据历史数据量,决定创建明日 index 对应的 shard 数量,也防止集中创建导致数据无法写入。现在日志系统只存近 7 天的业务日志,如果配置更久的保存时间的,会存到归档日志中。-对于存储来说,Hbase、Es 都是分布式系统,可以做到线性扩展。四、多租户随着日志系统不断发展,全网日志的 QPS 越来越大,并且部分用户对日志的实时性、准确性、分词、查询等需求越来越多样。为了满足这部分用户的需求,日志系统支持多租户的的功能,根据用户的需求,分配到不同的租户中,以避免相互影响。 针对单个租户的架构如下:-SDK:可以根据需求定制,或者采用天网的 TrackAppender 或 SkynetClient-Kafka 集群:可以共用,也可以使用指定 Kafka 集群-Spark 集群:目前的 Spark 集群是在 yarn 集群上,资源是隔离的,一般情况下不需要特地做隔离-存储:包含 ES 和 Hbase,可以根据需要共用或单独部署 ES 和 Hbase五、现有问题和未来规划目前,有赞日志系统作为集成在天网里的功能模块,提供简单易用的搜索方式,包括时间范围查询、字段过滤、NOT/AND/OR、模糊匹配等方式,并能对查询字段高亮显示,定位日志上下文,基本能满足大部分现有日志检索的场景,但是日志系统还存在很多不足的地方,主要有:1.缺乏部分链路监控:日志从产生到可以检索,经过多级模块,现在采集,日志缓冲层还未串联,无法对丢失情况进行精准监控,并及时推送告警。2.现在一个日志模型对应一个 kafka topic,topic 默认分配三个 partition,由于日志模型写入日志量上存在差异,导致有的 topic 负载很高,有的 topic 造成一定的资源浪费,且不便于资源动态伸缩。topic 数量过多,导致 partition 数量过多,对 kafka 也造成了一定资源浪费,也会增加延迟和 Broker 宕机恢复时间。3.目前 Elasticsearch 中文分词我们采用 ik_max_word,分词目标是中文,会将文本做最细粒度的拆分,但是日志大部分都是英文,分词效果并不是很好。上述的不足之处也是我们以后努力改进的地方,除此之外,对于日志更深层次的价值挖掘也是我们探索的方向,从而为业务的正常运行保驾护航。4月27日(周六)下午13:30,有赞技术中间件团队联合Elastic中文社区,围绕Elastic的开源产品及周边技术,在杭州举办一场线下技术交流活动。本次活动免费开放,限额200名。扫描下图二维码,回复“报名”即可参加欢迎参加,我们一起聊聊 ...

April 15, 2019 · 1 min · jiezi

你与解决“缓存污染”只差这篇文章的距离

微信公众号:IT一刻钟大型现实非严肃主义现场一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员关注可第一时间了解更多精彩内容,定期有福利相送哟。什么是缓存污染?由于缓存的读取速度比非缓存要快上很多,所以在高性能场景下,系统在读取数据时,是首先从缓存中查找需要的数据,如果找到了则直接读取结果,如果找不到的话,则从内存或者硬盘中查找,再将查找到的结果存入缓存,以备下次使用。实际上,对于一个系统来说,缓存的空间是有限且宝贵的,我们不可能将所有的数据都放入缓存中进行操作,即便可以数据安全性也得不到保证,而且,如果缓存的数据量过大大,其速度也会变得越来越慢。这个时候就需要考虑缓存的淘汰机制,但是淘汰哪些数据,又保留哪些数据,这是一个问题。如果处理不得当,就会造成“缓存污染”问题。而缓存污染,是指系统将不常用的数据从内存移到缓存,造成常用数据的挤出,降低了缓存效率的现象。解决缓存污染的算法LFU算法LFU,英文名Least Frequently Used,字面意思就是最不经常使用的淘汰掉算法,是通过数据被访问的频率来判断一个数据的热点情况。其核心理念是“历史上这个数据被访问次数越多,那么将来其被访问的次数也多”。LFU中每个数据块都有一个引用计数器,所有数据块按照引用数从大到小的排序。步骤:新数据插入到尾部,并将计数设置为1;当队列中的数据被访问后,引用计数+1,然后重新排序,保持引用次数从大到小排序;当空间不足,需要淘汰数据时,将尾部引用计数最小的数据块删除。分析:由于是根据频数进行热点判断和淘汰,所以先天具备避免偶发性、周期性批量操作导致临时非热点数据大量涌入缓存,挤出热点数据的问题。虽然具备这种先天优势,但依旧存在另一种缓存污染问题,即历史热点数据污染当前热点数据,如果系统访问模式发生了改变,新的热点数据需要计数累加超过旧热点数据,才能将旧热点数据进行淘汰,造成热点效应滞后的问题。复杂度与代价:每次操作都需要进行计数和排序,并且需要维护每个数据块计数情况,会占用较高的内存与cpu。一个小思考,根据LFU算法,如何以O(1)时间复杂度实现get和put操作缓存?LFU-Aging算法LFU-Aging是基于LFU的改进算法,目的是解决历史热点数据对当前热点数据的污染问题。有些数据在开始时使用次数很多,但以后就不再使用,这类数据将会长时间留在缓存中,所以“除了访问次数外,还要考虑访问时间”,这也是LFU-Aging的核心理念。虽然算法将时间纳入了考量范围,但LFU-Aging并不是直接记录数据的访问时间,而是增加了一个最大平均引用计数的阈值,然后通过当前平均引用计数来标识时间,换句话说,就是将当前缓存中的平均引用计数值当作当前的生命年代,当这个生命年代超过了预设的阈值,就会将当前所有计数值减半,形成指数衰变的生命年代。分析:优点是当访问模式发生改变的时候,生命年代的指数衰变会使LFU-Aging能够更快的适用新的数据访问模式,淘汰旧的热点数据。复杂度与代价:在LFU的基础上又增加平均引用次数判断和统计处理,对cpu的消耗更高,并且当平均引用次数超过指定阈值(Aging)后,还需要遍历每一个数据块的引用计数,进行指数衰变。Window-LFU算法Window-LFU顾名思义叫做窗口期LFU,区别于原义LFU中记录所有数据的访问历史,Window-LFU只记录过去一段时间内(窗口期)的访问历史,相当于给缓存设置了有效期限,过期数据不再缓存。当需要淘汰时,将这个窗口期内的数据按照LFU算法进行淘汰。分析:由于是维护一段窗口期的记录,数据量会比较少,所以内存占用和cpu消耗都比LFU要低。并且这段窗口期相当于给缓存设置了有效期,能够更快的适应新的访问模式的变化,缓存污染问题基本不严重。复杂度与代价:维护一段时期内的数据访问记录,并对其排序。LRU算法LRU算法,英文名Least Recently Used,意思是最近最少使用的淘汰算法,根据数据的历史访问记录来进行淘汰数据,核心思想是“如果数据最近被访问过1次,那么将来被访问的概率会更高”,类似于就近优先原则。步骤:新数据插入到链表头部;每当命中缓存,便将命中的缓存数据移到链表头部;当链表满的时候,将链表尾部的数据丢弃。分析:偶发性的、周期性的批量操作会使临时数据涌入缓存,挤出热点数据,导致LRU热点命中率急剧下降,缓存污染情况比较严重。复杂度与代价:数据结构复杂度较低;每次需要遍历链表,找到命中的数据块,然后将数据移到头部。LRU-K算法LRU-K是基于LRU算法的优化版,其中K代表最近访问的次数,从某种意义上,LRU可以看作是LRU-1算法,引入K的意义是为了解决上面所提到的缓存污染问题。其核心理念是从“数据最近被访问过1次”蜕变成“数据最近被访问过K次,那么将来被访问的概率会更高”。LRU-K与LRU区别是,LRU-K多了一个数据访问历史记录队列(需要注意的是,访问历史记录队列并不是缓存队列,所以是不保存数据本身的,只是保存对数据的访问记录,数据此时依旧在原始存储中),队列中维护着数据被访问的次数以及时间戳,只有当这个数据被访问的次数大于等于K值时,才会从历史记录队列中删除,然后把数据加入到缓存队列中去。步骤:数据第一次被访问时,加入到历史访问记录队列中,访问次数为1,初始化访问时间戳;如果数据访问次数没有达到K次,则访问次数+1,更新时间戳。当队列满了时,按照某种规则(LRU或者FIFO)将历史记录淘汰。为了避免历史数据污染未来数据的问题,还需要加上一个有效期限,对超过有效期的访问记录,进行重新计数。(可以使用懒处理,即每次对访问记录做处理时,先将记录中的访问时间与当前时间进行对比,如果时间间隔超过预设的值,则访问次数重置为1并更新时间戳,表示重新开始计数)当数据访问计数大于等于K次后,将数据从历史访问队列中删除,更新数据时间戳,保存到缓存队列头部中(缓存队列时间戳递减排序,越到尾部距离当前时间越长);缓存队列中数据被再次访问后,将其移到头部,并更新时间戳;缓存队列需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。分析:LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,一旦访问模式发生变化,需要大量的新数据访问才能将历史热点访问记录清除掉。复杂度与代价:LRU-K队列是一个优先级队列。由于LRU-K需要记录那些被访问过,但还没有放入缓存的对象,导致内存消耗会很多。URL-Two queues算法URL-Two queues算法类似于LRU-2,不同点在于URL-Two queues将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:URL-Two queues算法有两个缓存队列,一个是FIFO队列(First in First out,先进先出),一个是LRU队列。当数据第一次访问时,URL-Two queues算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。步骤:新访问的数据先插入到FIFO队列中;如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;如果数据在FIFO队列中被再次访问,则将数据从FIFO删除,加入到LRU队列头部;如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;LRU队列淘汰末尾的数据。分析:URL-Two queues算法和LRU-2算法命中率类似,但是URL-Two queues会减少一次从原始存储读取或计算数据的操作。命中率要高于LRU。复杂度与代价:需要维护两个队列,代价是FIFO和LRU代价之和。五三LRU算法emmmm…这个名字其实是我取的,大概是这种算法还没有被命名?当然,这是一个玩笑话。我是在mysql底层实现里发现这个算法的,mysql在处理缓存淘汰时是用的这个方法,有点像URL-Two queues的变体,只是我们只需要维护一个队列,然后将队列按照5:3的比例进行分割,5的那部分叫做young区,3的那部分叫做old区。具体是怎么样的请先看我把图画出来:步骤:第一次访问的数据从队列的3/8处位置插入;如果数据再次被访问,则移动到队列头部;如果数据没有被再访问,会逐步被热点数据驱逐向下移;淘汰尾部数据。分析:五三LRU算法算作是URL-Two queues算法的变种,原理其实是一样的,只是把两个队列合二为一个队列进行数据的处理,所以命中率和URL-Two queues算法一样。复杂度与代价:维护一个队列,代价较低,但是内存占用率和URL-Two queues一样。Multi Queue算法Multi Queue算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是“优先缓存访问次数多的数据”。Multi Queue算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级。访问优先级是根据访问次数计算出来的,例如:Q0,Q1….Qn代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数。步骤:新插入的数据放入Q0;每个队列按照LRU管理数据,再次访问的数据移动到头部;当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部;Q-history按照LRU淘汰数据的索引。分析:Multi Queue降低了“缓存污染”带来的问题,命中率比LRU要高。复杂度与代价:Multi Queue需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。Multi Queue需要记录每个数据的访问时间,需要定时扫描所有队列,代价比LRU要高。虽然Multi Queue的队列看起来数量比较多,但由于所有队列之和受限于缓存容量的大小,因此这里多个队列长度之和和一个LRU队列是一样的,因此队列扫描性能也相近。说在后面话还有哪些优秀的缓存淘汰算法,或者你有更好的想法或问题,欢迎留言给我!

April 10, 2019 · 1 min · jiezi

大规模手机定位采集系统设计

一、业务场景分析基本的业务需求可分为两大部分,第一部分是手机端间隔一定时间上报一次位置信息,第二部分是后台系统可以实时看看手机设备当前所在的位置,并绘制轨迹。总之就是用户安装了此应用,就相当于给自己装上了一个跟踪器,所到之处,都将有所记录。我们先来保守计算一组数据,假定用户基数为10万,每隔5秒上报一次位置信息,而这5秒期间,地图SDK大概会给出2次定位数据,由此得出,一次上报的瞬时峰值大概是20万条数据,一天将会达到300多万的数据。这样的QPS已经算是很高了,当然,绝大多数情况下是不会触顶的。另外一方面,后台系统在查询的时候,也面临着一个巨大的挑战:如何从海量的数据中找寻符合条件的那一部分?查询的条件无非是围绕三个维度展开的:时间、对象和区域。类似“查询某块地理位置栅栏内的所有用户在一段时间内的位置信息”,这就是一个典型的查询业务需求,涵盖了三个维度的条件。当然,还有一些距离的测算和排序的需求也是在所难免的。综上所述,我们要解决两个难点:1、上报用户的实时位置信息; 2、处理海量数据的读写请求。以上两个难点是任何LBS应用不得不攻克的,这是支撑业务发展的关键点。二、抽象的系统架构我们可以将整个请求链接进行拆解,分别用对应的策略解决这部分的难题:1、客户端收集数据。这一部分有四个基本要求:准实时,不丢数据,低功耗,高效率。2、网关限流。主要为了避免涌入大量无效请求,消耗系统资源,拖跨服务器。3、负载均衡。为了应对不断增长的访问量,服务实例必须支持横向扩展,多个实例之间按照一定的策略进行负载均衡。4、异步化读写数据。需要借助中间件,起到削峰的作用,这一部分的设计会重点阐述。5、数据存储层高可用。采集的数据不要求强一致性,依据CAP理论,满足AP两个条件即可。解决办法通常是集群化,避免单点故障,进而实现程序上的读写分离策略。三、技术选型接下来是针对上述五大部分在技术实现上的考量。目前客户端是苹果机和安卓机,未来很可能会接入友商的车载OS。在网络状况良好的时候,可以实时采集,实时上报,倘若网络状况不好,也不会丢数据,采用MMAP实现本地缓冲区。当然,为了上报效率,综合考虑还是按批次上传,过于频繁的上报也会致使耗电量攀升。网关限流和负载均衡交给了nginx,学习和开发成本低,重要的是效果很好,并且能够达到服务横向扩展的目的。我们使用了nginx基于ip地址的限流策略,同一个ip在1秒钟内最多允许3个请求,burst=5,为了应对突然的流量的爆发,还有一个nodelay参数,我们选择不加了,意味着请求队列和缓冲区都被塞满之后,直接返回503错误码,不再等待了。目前,是1个master进程,5个worker子进程,此外,超时时间、最大连接数以及缓冲区的设置值得斟酌,服务器性能好的话,可以设置得高一些。负载均衡的策略不是基于权重的设置,而是使用的最少连接 (least_conn),因为在我看来,每个服务实例都是可以被同等对待的,哪台空闲,就优先请求哪台。如果非要在此基础上设置一个weight,那就把性能高的服务器设置成较高的权重。为了应对大量的位置上报请求,必然需要引入消息队列。因为技术团队一直在使用RabbitMQ,并且一番压测后,表现依然坚挺,所以成为了我们的不二选择。此外,为了减轻存储层的压力,在数据落盘前有一个缓冲机制,是典型的主从双Buffer缓冲池,避免与存储层频繁交互,占用过多connection,提高了存储层的工作效率,但是缓冲池Flush的时候,会带来一次“IO尖刺”,可以调整缓冲池的Threshold,把“IO尖刺”控制在一个合理的范围内。最后,也是最引人关注的是存储层的选型。我们面临的选择比较多,我们预研了4种有可能的方案:MySQL,Redis,PostGIS,MongoDB。MySQL的方案有两条路可走,第一是使用纯SQL进行计算,很明显这条路子受限于数据量,随着数据量的增大,计算量剧增,系统性能急剧下降。第二是使用MySQL的Spatial Indexes,但是这类空间索引是MySQL5.7版本引入的,不巧的是,我们用的是阿里云RDS,MySQL版本是5.6.16,不得不放弃MySQL的方案。基于Redis GeoHash的方案,在性能方面是没任何问题的,但是有一个重大的缺憾,就是非常受限于距离查询,而无法方便的匹配另外的附加属性,也就是上述三维需求(时间、对象、区域)中,只能满足区域查询的需求,若想进一步过滤,还需要借助其他的存储技术。留下来的PostGIS和MongoDB是现在比较通用的解决方案。PostGIS的专业性很高,是地图服务商的首选,对OGC的标准支持得非常全面,提供的函数库也十分丰富。当然,MongoDB也不赖,3.0版本引入的WiredTiger存储引擎,给MongoDB增色了不少,性能和并发控制都有了较大幅度的提升。单就LBS应用来说,两者都可以很好的满足各方面的业务需求,性能也不是我们首要考虑的因素,因为两者的可扩展性都很强,不容易触及瓶颈。最后我们选择了MongoDB,是因为技术团队对此接受程度更高,MongoDB本来就是公司技术栈中的一员。PostGIS相对来说学习成本较高,因为Postgresql是一种关系型数据库,如果只接入普通的数据类型,其实相当于是维护了另一种MySQL,完全没必要,但是如果想用上GIS相关数据类型,现有的ORM框架支持得并不友好,开发效率低,维护成本高。四、异步读写设计出于对用户体验的考虑,不能因为读写速度慢让用户傻傻的等待,这类问题有一个通用的解决方案:异步化。异步化的写请求很好实现,如前述所说,通过消息中间件来解决此问题,对于客户端的位置上报请求,不需要强一致性,能确定数据被塞入消息队列中即可。除此之外,还在消息队列和存储层之间加入了双Buffer缓冲池,用以增加flush的效率。上图中的Buffer-1和Buffer-2共同组成一个循环队列,每次只有一个Buffer用了缓冲数据。图中的Flush Point表示每个Buffer达到一定的阈值(Threshold)后,将会触发数据落盘,并且会在此刻切换Buffer。图中所示的阈值是50%,可以根据实际情况上下调整。值得注意的是,在Flush Point,应该是先切换Buffer,再flush数据,也就是在flush的同时,还能接收上报的位置信息。如果要等待flush完毕再切换,将会导致流量过渡不平滑,出现一段时间的队列拥塞。接下来要解决大量读请求,应对这种问题,有一个不二法门就是:拆。非常类似“分表”的思想,但是在这个场景下,没有传统意义上“分表”所带来的诸多副作用。“分表”有一个规则,比如按Hash(user_id)进行“横向分表”,按照常用字段进行“纵向分表”。受到Hadoop关于资源管理的启发,在此位置采集系统中,使用了“元数据”来代替分表规则,这里的“元数据”相当于NameNode,是管理数据的数据,将散落在不同位置的数据集中化管理。如上图所示,按城市级别进行分库,每个城市的位置数据相对独立,体现在客户端的功能就是“切换城市”。“分表”意即分成多份Collection,以一天为最小粒度,因为一个城市的当天数据通常是“最热”的,众多查询请求都需要这份数据,分页,聚合,排序等需求都不是问题了,因此一天一个Collection是比较合理的切分。当然,如果一天的数据量过少,可以适当延长数据切分的周期。那么,如果查询请求的时间段跨天了,该怎么办呢?这时候,“元数据”就发挥威力了。查询请求分为两部分,第一个是定位元数据,就是在meta data中找寻符合条件的所有Collection(s),为了尽可能缩小查询范围,需要指定一定的时间区段,这个也是有现实意义的。所有的位置数据被分为了三种等级:Hot,Cold,Freeze,Hot等级通常是指当天的数据,读写最频繁的部分;Cold等级被界定为过去一个月内的数据,应对的各类查询和分析的需求;Freeze等级是已归档的数据,没有特殊情况不会被解冻。通过时间区段拿到N个Collection(s)后,附加上其他的查询条件,循环N次,成功循环一次,就渲染一次获取到的数据,直到循环结束。上述N>1的业务场景只可能出现在后台管理系统,也就是移动端不会出现跨天的业务需求,提供的API都是针对Hot级别的数据。如果一旦出现了这种情况,可以限制时间区段,比如只能获取两天内的数据。五、总结本文详细阐述了手机定位采集系统的设计,整个系统的复杂性集中在了数据的存储和呈现,我在此文中都给出了相应的解决方案。除此之外,整个采集系统还会有其他的功能模块,诸如日志采集与分析,实时监控等,限于篇幅,不再逐一叙述了。以下是扩展阅读,对于理解整个系统会有所帮助,建议阅读之。1、细说双Buffer缓冲池2、物联网设备网关系统架构设计扫描下方二维码,进入原创干货,搞“技”圣地。

April 9, 2019 · 1 min · jiezi

有赞美业店铺装修前端解决方案

一、背景介绍做过电商项目的同学都知道,店铺装修是电商系统必备的一个功能,在某些场景下,可能是广告页制作、活动页制作、微页面制作,但基本功能都是类似的。所谓店铺装修,就是用户可以在 PC 端进行移动页面的制作,只需要通过简单的拖拽就可以实现页面的编辑,属于用户高度自定义的功能。最终编辑的结果,可以在 H5、小程序进行展示推广。有赞美业是一套美业行业的 SaaS 系统,为美业行业提供信息化和互联网化解决方案。有赞美业本身提供了店铺装修的功能,方便用户自定义网店展示内容,下面是有赞美业店铺装修功能的截图:上面的图片是 PC 端的界面,下面两张图分别是 H5 和小程序的最终展示效果。可以简单地看到,PC 端主要做页面的编辑和预览功能,包括了丰富的业务组件和详细的自定义选项;H5 和小程序则承载了最终的展示功能。再看看有赞美业当前的技术基本面:目前我们的 PC 端是基于 React 的技术栈,H5 端是基于 Vue 的技术栈,小程序是微信原生开发模式。在这个基础上,如果要做技术设计,我们可以从以下几个角度考虑:三端的视图层都是数据驱动类型,如何管理各端的数据流程?三个端三种不同技术栈,业务中却存在相同的内容,是否存在代码复用的可能?PC 最终生成的数据,需要与 H5、小程序共享,三端共用一套数据,应该通过什么形式来做三端数据的规范管理?在扩展性上,怎么低成本地支持后续更多组件的业务加入?二、方案设计所以我们针对有赞美业的技术基本面,设计了一个方案来解决以上几个问题。首先摆出一张架构图:2.1 数据驱动首先关注 CustomPage 组件,这是整个店铺装修的总控制台,内部维护三个主要组件 PageLeft、 PageView 和 PageRight,分别对应上面提到的 PC 端3个模块。为了使数据共享,CustomPage 通过 React context 维护了一个”作用域“,提供了内部三个组件共享的“数据源”。 PageLeft 、 PageRight 分别是左侧组件和右侧编辑组件,共享 context.page 数据,数据变更则通过 context.pageChange 传递。整个过程大致用代码表示如下:// CustomerPageclass CustomerPage extends React.Component { static childContextTypes = { page: PropTypes.object.isRequired, pageChange: PropTypes.func.isRequired, activeIndex: PropTypes.number.isRequired, }; getChildContext() { const { pageInfo, pageLayout } = this.state; return { page: { pageInfo, pageLayout }, pageChange: this.pageChange || (() => void 0), activeIndex: pageLayout.findIndex(block => block.active), }; } render() { return ( <div> <PageLeft /> <PageView /> <PageRight /> </div> ); }}// PageLeftclass PageLeft extends Component { static contextTypes = { page: PropTypes.object.isRequired, pageChange: PropTypes.func.isRequired, activeIndex: PropTypes.number.isRequired, }; render() {…}}// PageRightclass PageRight extends Component { static contextTypes = { page: PropTypes.object.isRequired, pageChange: PropTypes.func.isRequired, activeIndex: PropTypes.number.isRequired, }; render() {…}}至于 H5 端,可以利用 Vue 的动态组件完成业务组件的动态化,这种异步组件的方式提供了极大的灵活性,非常适合店铺装修的场景。<div v-for=“item in components”> <component :is=“item.component” :options=“convertOptions(item.options)” :isEdit=“true”> </component></div>小程序因为没有动态组件的概念,所以只能通过 if else 的面条代码来实现这个功能。更深入的考虑复用的话,目前社区有开源的工具实现 Vue 和小程序之间的转换,可能可以帮助我们做的更多,但这里就不展开讨论了。PC 编辑生成数据,最终会与 H5、小程序共享,所以协商好数据格式和字段含义很重要。为了解决这个问题,我们抽取了一个npm包,专门管理3端数据统一的问题。这个包描述了每个组件的字段格式和含义,各端在实现中,只需要根据字段描述进行对应的样式开发就可以了,这样也就解决了我们说的扩展性的问题。后续如果需要增加新的业务组件,只需要协商好并升级新的npm包,就能做到3端的数据统一。/** * 显示位置 */export const position = { LEFT: 0, CENTER: 1, RIGHT: 2,};export const positionMap = [{ value: position.LEFT, name: ‘居左’,}, { value: position.CENTER, name: ‘居中’,}, { value: position.RIGHT, name: ‘居右’,}];2.2 跨端复用PageView 是预览组件,是这个设计的核心。按照最直接的思路,我们可能会用 React 把所有业务组件都实现一遍,然后把数据排列展示的逻辑实现一遍;再在 H5 和小程序把所有组件实现一遍,数据排列展示的逻辑也实现一遍。但是考虑到代码复用性,我们是不是可以做一些“偷懒”?如果不考虑小程序的话,我们知道 PC 和 H5 都是基于 dom 的样式实现,逻辑也都是 js 代码,两端都实现一遍的话肯定做了很多重复的工作。所以为了达到样式和逻辑复用的能力,我们想了一个方法,就是通过 iframe 嵌套 H5 的页面,通过 postmessage 来做数据交互,这样就实现了用 H5 来充当预览组件,那么 PC 和 H5 的代码就只有一套了。按照这个实现思路,PageView 组件可以实现成下面这样:class PageView extends Component { render() { const { page = {} } = this.props; const { pageInfo = {}, pageLayout = [] } = page; const { loading } = this.state; return ( <div className={style}> <iframe title={pageInfo.title} src={this.previewUrl} frameBorder=“0” allowFullScreen=“true” width=“100%” height={601} ref={(elem) => { this.iframeElem = elem; }} /> </div>); }}PageView 代码很简单,就是内嵌 iframe,其余的工作都交给 H5。H5 将拿到的数据,按照规范转换成对应的组件数组展示:<template> <div> <component v-for="(item, index) in components" :is=“item.component” :options=“item.options” :isEdit=“false”> </component> </div></template><script> computed: { components() { return mapToComponents(this.list); }, },</script>因为有了 iframe ,还需要利用 postmessage 进行跨源通信,为了方便使用,我们做了一层封装(代码参考自有赞餐饮):export default class Messager { constructor(win, targetOrigin) { this.win = win; this.targetOrigin = targetOrigin; this.actions = {}; window.addEventListener(‘message’, this.handleMessageListener, false); } handleMessageListener = (event) => { // 我们能相信信息的发送者吗? (也许这个发送者和我们最初打开的不是同一个页面). if (event.origin !== this.targetOrigin) { console.warn(${event.origin}不对应源${this.targetOrigin}); return; } if (!event.data || !event.data.type) { return; } const { type } = event.data; if (!this.actions[type]) { console.warn(${type}: missing listener); return; } this.actionstype; }; on = (type, cb) => { this.actions[type] = cb; return this; }; emit = (type, value) => { this.win.postMessage({ type, value, }, this.targetOrigin); return this; }; destroy() { window.removeEventListener(‘message’, this.handleMessageListener); }}在此基础上,业务方就只需要关注消息的处理,例如 H5 组件接收来自 PC 的数据更新可以这样用:this.messager = new Messager(window.parent, ${window.location.protocol}//mei.youzan.com);this.messager.on(‘pageChangeFromReact’, (data) => { …});这样通过两端协商的事件,各自进行业务逻辑处理就可以了。这里有个细节需要处理,因为预览视图高度会动态变化,PC 需要控制外部视图高度,所以也需要有动态获取预览视图高度的机制。// vue scriptupdated() { this.$nextTick(() => { const list = document.querySelectorAll(’.preview .drag-box’); let total = 0; list.forEach((item) => { total += item.clientHeight; }); this.messager.emit(‘vueStyleChange’, { height: total }); }}// react scriptthis.messsager.on(‘vueStyleChange’, (value) => { const { height } = value; height && (this.iframeElem.style.height = ${height}px);});2.3 拖拽实现拖拽功能是通过 HTML5 drag & drop api 实现的,在这次需求中,主要是为了实现拖动过程中组件能够动态排序的效果。这里有几个关键点,实现起来可能会花费一些功夫:向上向下拖动过程中视图自动滚动拖拽结果同步数据变更适当的动画效果目前社区有很多成熟的拖拽相关的库,我们选用了vuedraggable。原因也很简单,一方面是避免重复造轮子,另一方面就是它很好的解决了我们上面提到的几个问题。vuedraggable 封装的很好,使用起来就很简单了,把我们前面提到的动态组件再封装一层 draggable 组件:<draggable v-model=“list” :options=“sortOptions” @start=“onDragStart” @end=“onDragEnd” class=“preview” :class="{dragging: dragging}"> <div> <component v-for="(item, index) in components" :is=“item.component” :options=“item.options” :isEdit=“false”> </component> </div></draggable>const sortOptions = { animation: 150, ghostClass: ‘sortable-ghost’, chosenClass: ‘sortable-chosen’, dragClass: ‘sortable-drag’,};// vue scriptcomputed: { list: { get() { return get(this.designData, ‘pageLayout’) || []; }, set(value) { this.designData.pageLayout = value; this.notifyReact(); }, }, components() { return mapToComponents(this.list); },},三、总结到这里,所有设计都完成了。总结一下就是:PC 端组件间主要通过 React context 来做数据的共享;H5 和 小程序则是通过数据映射对应的组件数组来实现展示;核心要点则是通过 iframe 来达到样式逻辑的复用;另外可以通过第三方npm包来做数据规范的统一。当然除了基本架构以外,还会有很多技术细节需要处理,比如需要保证预览组件不可点击等,这些则需要在实际开发中具体处理。 ...

April 8, 2019 · 3 min · jiezi

架构整洁之道(一)——什么是设计和架构(未完)

在过去几年里,关于设计和架构存在很多疑问。什么是设计,什么是架构,两者有什么不同?本书的其中一个目的就是消除所有的困惑,并且彻底地给一个确切的定义,设计和架构是什么.对于初学者来说这两者没有区别,一点也没有架构这个词经常被用在一些高级别的,与低级别的细节分离的上下文当中.而”设计“经常看起来暗示在低级别的决议和结构。但是当你看到架构师做的架构,这种说法又是荒谬的。思考一下,假如架构师设计你的房子,这个房子有架构吗?当然有。那它的架构又是什么呢。是它的形状,外貌,立体面图,空间和房间的布局。但是当我浏览架构师画出的图。我看到了大量的低级别的细节实现,我看见每一个插座,电灯开关,电灯被标注出来。我看见哪些开关控制哪些电灯。我看到了放置火炉的位置,热水器和抽水泵的大小和位置都被标注了出来,我看见了墙,屋顶,地基将如何被搭建简而言之,我看见全部的支持顶层决策的底层细节,也看见了底层细节和顶层决策是整个房屋设计的一部分.所以这就是软件设计,底层细节和顶层结构是相同物体的一部分.它们组成了一个连续的,能定义系统形状的构造物。两者缺一不可。两者没有明确的分割线,软件设计的目标好的软件设计的目标和我乌托邦式的描述一模一样。软件架构的目标是用最少的人力资源搭建和维护要求的系统。设计质量的权衡也就是满足用户需求所付出的成本的权衡.如果成本很低,并且至始至终都很低。这就是一个优秀的设计。如果成本很高,而且每次发布新版本,成本逐渐上升。这种设计就比较差。很简单。实例学习举个例子,思考下接下来的实例学习。它包含了来自匿名公司的真实数据,首先,让我们看看工程师的增长,我敢肯定你你会觉得这个倾向很振奋人心。让我们看看该公司同时期的产出,靠代码条形图来衡量很明显有些东西正在变糟,即使每个版本都有在逐渐增加的工程师支持,但代码的增长接近不对称.现在来看一张图,这张图展示了每行代码的维护成本这种倾向不是可持续发展的,不管此刻公司是多么盈利。是什么造成了这种明显的变化,为什么代码从第8版到第1版贵了40多倍

March 31, 2019 · 1 min · jiezi

coder,你会设计交易系统吗(概念篇)?

文中我们从严谨的角度一步步聊到支付如何演变成独立的系统。内容包括:系统演进过程、接口设计、数据库设计以及代码如何组织的示例。若有不足之处,欢迎讨论共同学习。从模块到服务我记得最开始工作的时候,所有的功能:加购物车/下单/支付 等逻辑都是放在一个项目里。如果一个新的项目需要某个功能,就把这个部分的功能包拷贝到新的项目。数据库也原封不动的拷贝过来,稍微根据需求改改。这就是所谓的 单体应用 时代,随着公司产品线开始多元,每条产品线都需要用到支付服务。如果支付模块调整了代码,那么就会处处改动、处处测试。另一方面公司的交易数据割裂在不同的系统中,无法有效汇总统一分析、管理。这时就到了系统演进的时候,我们把每个产品线的支付模块抽离成统一的服务。对自己公司内部提供统一的API使用,可以对这些API进一步包装成对应的SDK,供内部业务线快速接入。这里服务使用HTTP或者是RPC协议都可以根据公司实际情况决定。不过如果考虑到未来给第三方使用,建议使用HTTP协议,系统的演变过程:总结下,将支付单独抽离成服务后,带来好处如下:避免重复开发,数据隔离的现象出现;支付系统周边功能演进更容易,整个系统更完善丰满。如:对账系统、实时交易数据展示;随时可对外开发,对外输出Paas能力,成为有收入的项目;专门的团队进行维护,系统更有机会演进成顶级系统;公司重要账号信息保存一处,风险更小。系统能力如果我们接手该需求,需要为公司从零搭建支付系统。我们该从哪些方面入手?这样的系统到底需要具备什么样的能力呢?首先支付系统我们可以理解成是一个适配器。他需要把很多第三方的接口进行统一的整合封装后,对内部提供统一的接口,减少内部接入的成本。做为一个最基本的支付系统。需要对内提供如下接口出来:发起支付,我们取名:/gopay发起退款,我们取名:/refund接口异步通知,我们取名:/notify/支付渠道/商户交易号接口同步通知,我们取名:/return/支付渠道/商户交易号交易查询,我们取名:/query/trade退款查询,我们取名:/query/refund账单获取,我们取名:/query/bill结算明细,我们取名:/query/settle一个基础的支付系统,上面8个接口是肯定需要提供的(这里忽略某些支付中的转账、绑卡等接口)。现在我们来基于这些接口看看都有哪些系统会用到。下面按照系统维度,介绍下这些接口如何使用,以及内部的一些逻辑。应用系统一般支付网关会提供两种方式让应用系统接入:网关模式,也就是应用系统自己需要开发一个收银台;(适合提供给第三方)收银台模式,应用系统直接打开支付网关的统一收银台。(内部业务)下面为了讲清楚设计思路,我们按照 网关模式 进行讲解。对于应用系统它需要能够请求支付,也就是调用 gopay 接口。这个接口会处理商户的数据,完成后会调用第三方网关接口,并将返回结果统一处理后返回给应用方。这里需要注意,第三方针对支付接口根据我的经验大致有以下情况:支付时,不需要调用第三方,按照规则生成数据即可;支付时,需要调用第三方多个接口完成逻辑(这可能比较慢,大型活动时需要考虑限流/降配);返回的数据是一个url,可直接跳转到第三方完成支付(wap/pc站);返回的数据是xml/json结构,需要拼装或作为参数传给她的sdk(app)。这里由于第三方返回结构的不统一,我们需要统一处理成统一格式,返回给商户端。我推荐使用json格式。{ “errno”:0, “msg”:“ok”, “data”:{ }}我们把所有的变化封装在 data 结构中。举个例子,如果返回的一个url。只需要应用程序发起 GET 请求。我们可以这样返回:{ “errno”:0, “msg”:“ok”, “data”:{ “url”:“xxxxx”, “method”:“GET” }}如果是返回的结构,需要应用程序直接发起 POST 请求。我们可以这样返回:{ “errno”:1, “msg”:“ok”, “data”:{ “from”:"<form action=“xxx” method=“POST”>xxxxx</form>", “method”:“POST” }}这里的 form 字段,生成了一个form表单,应用程序拿到后可直接显示然后自动提交。当然封装成 from表单这一步也可以放在商户端进行。上面的数据格式仅仅是一个参考。大家可根据自己的需求进行调整。一般应用系统除了会调用发起支付的接口外,可能还需要调用 支付结果查询接口。当然大多数情况下不需要调用,应用系统对交易的状态只应该依赖自己的系统状态。对账系统对于对账,一般分为两个类型:交易对账 与 结算对账交易对账交易对账的核心点是:检查每一笔交易是否正确。它主要目的是看我们系统中的每一笔交易与第三方的每一笔交易是否一致。这个检查逻辑很简单,对两份账单数据进行比较。它主要是使用 /query/bill 接口,拿到在第三方那边完成的交易数据。然后跟我方的交易成功数据进行比较。检查是否存在误差。这个逻辑非常简单,但是有几点需要大家注意:我方的数据需要正常支付数据+重复支付数据的总和;对账检查不成功主要包括:金额不对、第三方没有找到对应的交易数据、我方不存在对应的交易数据。针对这些情况都需要有对应的处理手段进行处理。在我的经验中上面的情况都有过遇到。金额不对:主要是由于第三方的问题,可能是系统升级故障、可能是账单接口金额错误;第三方无交易数据: 可能是拉去的账单时间维度问题(比如存在时差),这种时区问题需要自己跟第三方确认找到对应的时间差。也可能是被攻击,有人冒充第三方异步通知(说明系统校验机制又问题或者密钥泄漏了)。自己系统无交易数据: 这种原因可能是第三方通知未发出或者未正确处理导致的。上面这些问题的处理绝大部份都可以依赖 query/trade query/refund 来完成自动化处理。结算对账那么有了上面的 交易对账 为什么还需要 结算对账 呢?这个系统又是干嘛的?先来看下结算的含义。结算,就是第三方网关在固定时间点,将T+x或其它约定时间的金额,汇款到公司账号。下面我们假设结算周期是: T+1。结算对账主要使用到的接口是 /query/settle,这个接口获取的主要内容是:每一笔结算的款项都是由哪些笔交易组成(交易成功与退款数据)。以及本次结算扣除多少手续费用。它的逻辑其实也很简单。我们先从自己的系统按照 T+1 的结算周期,计算出对方应该汇款给我们多少金额。然后与刚刚接口获取到的数据金额比较:银行收款金额 + 手续费 = 我方系统计算的金额这一步检查通过后,说明金额没有问题。接下来需要检查本次结算下的每一笔订单是否一致。结算系统是 强依赖 对账系统的。如果对账发现异常,那么结算金额肯定会出现异常。另外结算需要注意的一些问题是:银行可能会自行退款给用户,因为用户可直接向自己发卡行申请退款;结算也存在时区差问题;结算接口中的明细交易状态与我方并不完全一致。比如:银行结算时发现某笔退款完成,但我方系统在进行比较时按照未退款完成的逻辑在处理。针对上面的问题,大家根据自己的业务需求需要做一些方案来进行自动化处理。财务系统财务系统有很多内部业务,我这里只聊与支付系统相关的。(当然上面的对账系统也可以算是财务范畴)。财务系统与支付主要的一个关系点在于校验交易、以及退款。这里校验交易可以使用 query/trade query/refund这两个接口来完成。这个逻辑过程就不需要说了。下面重点说下退款。我看到很多的系统退款是直接放在了应用里边,用户申请退款直接就调用退款接口进行退款。这样的风险非常高。支付系统的关于资金流向的接口一定要慎重,不能过多的直接暴露给外部,带来风险。退款的功能应该是放到财务系统来做。这样可以走内部的审批流程(是否需要根据业务来),并且在财务系统中可以进行更多检查来觉得是否立即进行退款,或者进入等待、拒绝等流程。第三方网关针对第三方主要使用到的其实就是异步通知与同步通知两个接口。这一部分的逻辑其实非常简单。就是根据第三方的通知完成交易状态的变更。以及通知到自己对应的应用系统。这部分比较复杂的是,第三方的通知数据结构不统一、通知的类型不统一。比如:有的退款是同步返回结果、有的是异步返回结果。这里如何设计会在后面的 系统设计 中给出答案。第一部份的内容就到此结束了。如果有什么疑问欢迎到我们GitHub主页留言。GitHub: https://github.com/skr-shop

March 11, 2019 · 1 min · jiezi

模块融合中的一些思考

合久必分,分久必合。模块拆分通常并非架构RD因重构而自发的,新的业务需求或产品线要求快速上线,从现有成熟模块迁移过来,稍加改动就能实现目标,这常常皆大欢喜。然而,随着不同产品线架构的迭代和调整,两个同源模块的差异越来越大,但各个模块内部似乎仍能保持良好的框架和模块。也许是RD固有的处女座自虐倾向,或许是对架构统一的偏执,在经过各自发展后的两个模块现在提出融合要求,不管是对人力成本的考虑还是对形成僵尸代码的担忧,总之,一旦融合提上日程,这对两个模块以及负责融合的RD来讲,都是一场灾难。融合是一项吃力不讨好的工作,容易背锅,没有“收益”。本文是这个悲惨的RD在融合过程中的一些思考和总结(待融合模块10w+代码行)。1、融合需要对模块有极为细致的理解。融合需要对两个模块都有较为深入的理解,融合通常由其中一个模块的RD负责,对另一模块并不一定十分了解,那么就需要特别注重融合前期的准备工作。一方面,对模块的架构模型、关键流程、实现方式进行理解和分析(全局);另一方面,对模块的实现细节、内部结构、流程分支进行梳理和记录(细节)。这个过程是十分耗时的,但是对后续融合工作的开展提供了坚实的基础。2、融合的原则是什么?融合的原则总结为以下几点:1)能够复用的结构、功能和流程尽量复用,避免添加特有逻辑破坏模块架构的统一性;2)相对独立的流程尽量保持代码的独立性,避免模块后续调整带来的高耦合问题;3)合理屏蔽融合过程中不需要的流程和函数,避免添加一堆if/else,合理利用现有条件空转过滤。3、推荐的开发方式。融合常常是一个漫长的过程,同时,在融合过程中模块还会不断进行架构的迭代开发,因为,融合工作推荐采用快速迭代开发方式。开发、测试、上线、开发……,合理划分出每个阶段的边界,不管是对开发者、评审人、QA来说都更加清晰,上线风险也能够有效管控。而被融合的模块在融合工作完成后,再由QA进行全面的系统、性能测试,保证两者的功能均能达到预期。4、融合对模块的影响。首先,融合后最基本的要求是原有功能不受影响,相互独立实现各自功能;需要格外关注的是,待融合的两个模块的性能变化。一般情况下,融合后由于模块中处理流程更加复杂,处理时延会有明显增加;另外,内存使用量、CPU也将会有性能上的损失,一方面要和融合前独立模块时的性能进行比较,另一方面,要考虑如果部署集群也统一运维,集群在接入新流量后是否还能扛住。

February 27, 2019 · 1 min · jiezi

一文看透浏览器架构

本文由云+社区发表作者:廖彩明在从事前端开发过程中,浏览器作为最重要的开发环境,浏览器基础是是前端开发人员必须掌握的基础知识点,它贯穿着前端的整个网络体系。对浏览器原理的了解,决定着编写前端代码性能的上限。浏览器作为JS的运行环境,学习总结下现代浏览器的相关知识前言经常听说浏览器内核,浏览器内核究竟是什么,以及它做了什么。我们将来了解下浏览器的主要组成部分、现代浏览器的主要架构、浏览器内核、浏览器内部是如何工作的1 浏览器现代浏览器结构如下:The browser’s main componentThe User Interface主要提供用户与Browser Engine交互的方法。其中包括:地址栏(address bar)、向前/退后按钮、书签菜单等等。浏览器除了渲染请求页面的窗口外的所有地方都属于The User InterfaceThe Browser Engine协调(主控)UI和the Rendering Engine,在他们之间传输指令。 提供对The Rendering Engine的高级接口,一方面它提供初始化加载Url和其他高级的浏览器动作(如刷新、向前、退后等)方法。另一方面Browser Engine也为User Interface提供各种与错误、加载进度相关的消息。The Rendering Engine为给定的URL提供可视化的展示。它解析JavaScript、Html、Xml,并且User Interface中展示的layout。其中关键的组件是Html解析器,它可以让Rendering Engine展示差乱的Html页面。 值得注意:不同的浏览器使用不同的Rendering Engine。例如IE使用Trident,Firefox使用Gecko,Safai使用Webkit。Chrome和Opera使用Webkit(以前是Blink)The Networking基于互联网HTTP和FTP协议,处理网络请求。网络模块负责Internet communication and security,character set translations and MIME type resolution。另外网络模块还提供获得到文档的缓存,以减少网络传输。为所有平台提供底层网络实现,其提供的接口与平台无关The JavaScript Interpreter解释和运行网站上的js代码,得到的结果传输到Rendering Engine来展示。The UI Backend用于绘制基本的窗口小部件,比如组合框和窗口。而在底层使用操作系统的用户界面方法,并公开与平台无关的接口。The Data Storage管理用户数据,例如书签、cookie和偏好设置等。2 主流浏览器的架构2.1 FireFoxFireFox的架构可以看到火狐浏览器的渲染引擎(Rendering Engine)使用的是Gecko;XML Parser解析器是Expat;Java Script解释器是Spider-Monkey(c语言实现)2.2 ChromeChrome的架构渲染引擎Rendering Engine使用的是WebKitXML Parser: libXML解析XML,libXSLT处理XSLTJS解释器使用C++实现的V8引擎,2.3 IEIE的架构渲染引擎主要是TridentScripting Engine有JScript和VBScript3 浏览器内核浏览器最重要或者说核心的部分是“Rendering Engine”,可大概译为“渲染引擎”,不过我们一般习惯将之称为“浏览器内核”。主要包括以下线程:3.1 浏览器 GUI 渲染线程,主要包括: HTML Parser 解析HTML文档,将元素转换为树结构DOM节点,称之为Content Tree CSS Parser 解析Style数据,包括外部的CSS文件以及在HTML元素中的样式,用于创建另一棵树,调用“Render Tree” Layout过程 为每个节点计算出在屏幕中展示的准确坐标 Painting 遍历Render Tree,调用UI Backend提供的接口绘制每个节点3.2 JavaScript 引擎线程JS引擎线程负责解析Javascript脚本,运行代码 JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞a) 减少 JavaScript 加载对 DOM 渲染的影响(将 JavaScript 代码的加载逻辑放在 HTML 文件的尾部,减少对渲染引擎呈现工作的影响;b) 避免重排,减少重绘(避免白屏,或者交互过程中的卡顿;c) 减少 DOM 的层级(可以减少渲染引擎工作过程中的计算量;d) 使用 requestAnimationFrame 来实现视觉变化(一般来说我们会使用 setTimeout 或 setInterval 来执行动画之类的视觉变化,但这种做法的问题是,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿)3.3 浏览器定时触发器线程浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案3.4 浏览器事件触发线程当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JavaScript 的单线程关系所有这些事件都得排队等待 JavaScript 引擎处理。3.5 浏览器 http 异步请求线程在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。4 以Chrome浏览器为例,演示浏览器内部如何工作上面铺垫了这么多理论,下面结合Chrome讲解当用户在地址栏上输入URL后,浏览器内部都做了写什么4.1 Chrome浏览器中的多进程打开Chrome 任务管理器,可以看到Chrome运行的进程各个进程的功能• Browser进程功能:Controls “chrome” part of the application including address bar, bookmarks, back and forward buttons. Also handles the invisible, privileged parts of a web browser such as network requests and file access.• GPU进程功能:Handles GPU tasks in isolation from other processes. It is separated into different process because GPUs handles requests from multiple apps and draw them in the same surface.• 第三方插件进程功能:Controls any plugins used by the website, for example, flash. 每个插件对应一个进程,当插件运行时创建• 浏览器渲染进程功能:Controls anything inside of the tab where a website is displayed. 默认每个标签页创建一个渲染引擎实例。• V8 Proxy resolver关于V8 Proxy resolver可查看code.google.comgroup.google.com https://groups.google.com/a/c...!topic/net-dev/73f9B5vFphI doc.google.comChrome支持使用代理脚本为给定的网址选择代理服务器,包含使用操作系统提供的代理解析程序的多个平台的回退实现。但默认情况下(iOS除外),它使用内置的解析V8执行代理脚本(V8 pac)。今天(截至2015年1月),V8 pac在浏览器进程中运行。这意味着浏览器进程包含一个V8实例,这是一个潜在的安全漏洞。在浏览器进程中允许V8还需要浏览器进程允许写入 - 执行页面。我们关于将V8 pac迁移到单独进程的建议包括为解析器创建Mojo服务,从实用程序进程导出该服务,以及从浏览器进程创建/连接到该进程。浏览器进程之间主要通过IPC (Inter Process Communication)通信4.2 Per-frame renderer processes - Site IsolationSite Isolation is a recently introduced feature in Chrome that runs a separate renderer process for each cross-site iframe. We’ve been talking about one renderer process per tab model which allowed cross-site iframes to run in a single renderer process with sharing memory space between different sites. Running a.com and b.com in the same renderer process might seem okay. The Same Origin Policy is the core security model of the web; it makes sure one site cannot access data from other sites without consent. Bypassing this policy is a primary goal of security attacks. Process isolation is the most effective way to separate sites. With Meltdown and Spectre, it became even more apparent that we need to separate sites using processes. With Site Isolation enabled on desktop by default since Chrome 67, each cross-site iframe in a tab gets a separate renderer process.每个iframe是单独的渲染进程此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

February 25, 2019 · 2 min · jiezi

分布式系统「伸缩性」大招之——「弹性架构」详解

如果第二次看到我的文章,欢迎下方扫码订阅我的个人公众号(跨界架构师)哟本文长度为3633字,建议阅读10分钟。坚持原创,每一篇都是用心之作~如果我们的开发工作真的就如搭积木一般就好了,轮廓分明,个个分开,坏了哪块积木换掉哪块就好了。但是,实际我们的工作中所面临的可能只有一块积木,而且还是一大块,要换得一起换,要修得一起修。Z哥在之前《分布式系统关注点(13)——「高内聚低耦合」详解》中提到的分层架构它可以让我们有意识的去做一些切分,但是换和修的难度还是根据切分的粒度大小来决定的。有更好的方式吗?这是显然的。事件驱动架构我们来换一个思维看待这个问题。不管是平时的系统升级也好、修复bug也好、扩容也好,其实就是一场“手术”。通过这场“手术”来解决当前面临的一些问题。那么分层架构好比只是将一个人的手、脚、嘴、鼻等分的清清楚楚,但是整体还是紧密的耦合在一起。怎么耦合的呢?我们人是靠“血液”的流动连接起来的。这就好比在分布式系统中通过rpc框架连接起不同的节点一样。但是软件与人不同,有2种不同的连接方式,除了「同步」的方式之外还有「异步」的方式。因为有些时候你不需要知道其他系统的执行结果,只要确保自己将其需要的数据传递给它了即可。恰巧有一种架构是这种模式的典型——事件驱动架构(简称EDA,Event Driven Architecture)。平时常见的MQ、本地消息表等运用于数据传递的中转环节,就是事件驱动架构的思想体现。事件驱动架构又细分为两种典型的实现方式,与Z哥之前在《分布式系统关注点(3)——「共识」的兄弟「事务」》中提到的Saga模式的2种实现方式类似,一种是中心化的、一种是去中心化的。下面Z哥来举个例子,让你看起来更容易理解一些。(例子仅为了阐述是怎么工作的,真正的实施中还需要考虑如何保证数据一致性等问题,这部分可以参考之前发表的系列文章,文末带传送门)传统的电商场景中,用户从购物车中点击“提交”按钮后,至少需要做这几件事:生成一笔订单、生成一笔支付记录、给订单匹配发货的快递公司。在这个场景下,中心化和去中心化有什么不同呢?中心化这种模式拥有一个“上帝”。但是“上帝”不会处理也不知道任何业务逻辑,它只编排事件。除了中心化之外,它还有什么特点呢?Z哥给它的定义是“3+2结构”。这种模式中存在3种类型的主体:事件生产者、“上帝”(调停者)、事件处理者。然后中间夹着两层队列,以此结构就能解耦。就像这样:事件生产者 –> 队列 –> “上帝”(调停者) –> 队列 –> 事件处理者。那么回上面的这个例子中,事件生产者CartService发出了一个“订单创建”事件,通过队列传递给调停者。然后调停者根据事先制定好的编排规则对事件进行相应的转换,也通过队列做二次分发,传递给事件处理者。可能你会问,这些好理解。但是,我之前也经常看到什么编排编排的,到底编排该怎么做呢?其实编排主要做两件事:「事件转换」和「事件发送」(对应「服务编排」类框架的「调用」)。「事件转换」实质就是给将要发送的事件对象的参数进行赋值。赋值的数据来源于哪呢?除了事件产生的源头带入的参数,还有持续累积的「上下文」,就如下图中这样的一个共享存储空间。可能你又会问,我怎么将多个事件处理者之间组合成一个上下文呢?通过一个全局唯一的标识即可,每次向“上帝”丢事件的时候把这个全局唯一标识带过去。题外话:一般来说,还会在一个全局唯一标识之下带一个内部唯一的「子流水号」,为了配合做接下去要讲到的「事件发送」。一是为了后续排查问题的时候清晰的知道这次调用产生的异常是从哪个上游系统来的。二是为了便于观测整个调用的逻辑是否符合编排时的预期。怎么做呢?通过一个x.x.x.x格式的序号。比如,串行就是1,2,3。分支和并行就是2.1,2.2。分支+串行的结合就是1,2,2.1,2.2,3。「事件发送」实质就是负责事件流转的逻辑控制,然后发往「事件处理者」去处理。它决定了是按顺序还是分支进行?是串行还是并行?到这就不再展开了,要不然就跑题了,我们下次再细聊这部分内容。再强调一下,「事件转换」和「事件发送」是你在实现“上帝”(调停者)功能的时候需要满足的最基本的两个功能哦。中心化最大的优势是让流程更加的“可见”了,同时也更容易去做一些监控类的东西,系统规模越大,这个优势产生的效果越明显。但是一个最基本的“上帝”(调停者)实现起来还需要考虑数据一致性问题,所以,会大大增加它的实现复杂度。因此,如果你面对的场景,业务没有特别庞大,并且是比较稳定的,或许用去中心化的方式也是不错的选择。去中心化这个模式由于没有了“上帝”,因此每个事件处理者需要知道自己的下一个事件处理器是什么?需要哪些参数?以及队列是哪个之类的东西。但是整体结构会变得简单很多,从“3+2结构”变成了“2+1结构”。结构简化背后的复杂度都跑到事件处理者开发人员编写的业务代码中去了。因为他需要自己去负责「事件转换」和「事件发送」这两个事情。嗯,改造成事件驱动架构之后,通过「队列」的解耦和异步的事件流转,系统的运转的确会更顺畅。但是有时候你可能想进行更细粒度的控制,因为一般情况下,一个service中会处理很多业务环节,不太会只存在一个对外接口、一条业务逻辑。在这样的情况下,很多时候你可能需要修改的地方仅仅是其中的一个接口。能不能只修改其中的一部分代码并且进行「热更新」呢?微内核架构(插件架构)就适合来解决这个问题。微内核架构顾名思义,微内核架构的关键是内核。所以需要先找到并明确内核是什么?然后将其它部分都视作“可拆卸”的部件。好比我们一个人,大脑就是内核,其它的什么都可以换,换完之后你还是你,但是大脑换了就不是你了。微内核架构整体上由两部分组成:核心系统和插件模块。核心系统内又包含了微内核、插件模块,以及内置的一些同样以插件形式提供的默认功能。其中,微内核主要负责插件的生命周期管理和控制插件模块。插件模块则负责插件的加载、替换和卸载。外部的插件如果要能够接入进来并顺利运行,前提先要有一个满足标准接口规范的实现。一个插件的标准接口至少会有这样的2个方法需要具体的插件来实现:public interface IPlugin{ /// <summary> /// 初始化配置 /// </summary> void InitializeConfig(Dictionary<string,string> configs); /// <summary> /// 运行 /// </summary> void Run(); …}最后,插件之间彼此独立,但核心系统知道哪里可以找到它们以及如何运行它们。 最佳实践知道了这两种具有“弹性”的架构模式,你该如何判断什么情况下需要搬出来用呢?Z哥带你来分析一下每一种架构的优缺点,就能发现它适用的场景。事件驱动架构它的优点是:通过「队列」进行解耦,使得面对快速变化的需求可以即时上线,而不影响上游系统。由于「事件」是一个独立存在的“标准化”沟通载体,可以利用这个特点衔接各种跨平台、多语言的程序。如果再进行额外的持久化,还能便于后续的问题排查。同时也可以对「事件」进行反复的「重放」,对处理者的吞吐量进行更真实的压力测试。更“动态”、容错性好。可以很容易,低成本地集成、再集成、再配置新的和已经存在的事件处理者,也可以很容易的移除事件处理者。轻松的做扩容和缩容。在“上帝”模式下,对业务能有一个“可见”的掌控,更容易发现流程不合理或者被忽略的问题。同时能标准化一些技术细节,如「数据一致性」的实现方式等。它的缺点是:面对不稳定的网络问题、各种异常,想要处理好这些以确保一致性,需要比同步调用花费很大的精力和成本。无法像同步调用一般,操作成功后即代表可以看到最新的数据,需要容忍延迟或者对延迟做一些用户体验上的额外处理。那么,它所适用的场景就是:对实时性要求不高的场景。系统中存在大量的跨平台、多语言的异构环境。以尽可能提高程序复用度为目的的场景。业务灵活多变的场景。需要经常扩容缩容的场景。微内核架构它的优点是:为递进设计和增量开发提供了方便。可以先实现一个稳固的核心系统,然后逐渐地增加功能和特性。和事件驱动架构一样,也可避免单一组件失效,而造成整个系统崩溃,容错性好。内核只需要重新启动这个组件,不致于影响其他功能。它的缺点是:由于主要的微内核很小,所以无法对整体进行优化。每个插件都各自管各自的,甚至可能是由不同团队负责维护。一般来说,为了避免在单个应用程序中的复杂度爆炸,很少会启用插件嵌套插件的模式,所以插件中的代码复用度会差一些。那么,它所适用的场景就是:可以嵌入或者作为其它架构模式的一部分。例如事件驱动架构中,“上帝”的「事件转换」就可以使用微内核架构实现。业务逻辑虽然不同,但是运行逻辑相同的场景。比如,定期任务和作业调度类应用。具有清晰的增量开发预期的场景。总结好了,我们总结一下。这次呢,Z哥向你介绍了「事件驱动架构」的两种实现模式和实现思路,以及「微内核架构」的实现思路。并且奉上了对这两种架构模式的优缺点与适用场景分析的最佳实践。希望对你有所启发。相关文章:分布式系统关注点(1)——初识数据一致性分布式系统关注点(2)——通过“共识”达成数据一致性分布式系统关注点(3)——「共识」的兄弟「事务」作者:Zachary出处:https://www.cnblogs.com/Zacha…如果你喜欢这篇文章,可以点一下文末的「赞」。这样可以给我一点反馈。: )谢谢你的举手之劳。▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码。定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

February 18, 2019 · 1 min · jiezi

从零搭建精准运营系统

2018刚过去,趁着春节放假对过去一年主导开发的项目做个梳理和总结项目背景平台运营到一定阶段,一定会累积大批量的用户数据,这些用户数据是运营人员的黄金财产。而如何利用用户的数据来做运营(消息推送、触达消息、优惠券发送、广告位等),正是精准运营系统需要解决的问题。本文是基于信贷业务实践后写出来的,其它行业如保险、电商、航旅、游戏等也可以参考。业务场景先看几个具有代表性的需求用户可用额度在20000~50000元,而且有借款记录,未还本金为0,性别为“男”用户发生了A行为且未还本金大于5000用户在1天内发生A行为次数大于等于3次用户在A行为前24小时内未发生B行为用户在A行为后一个月内未发生B行为业务上有两种消息类型日常消息:由业务人员通过条件筛选锁定用户群,定时或即时给批量用户发送消息或者优惠券触达消息:主要由用户自身的行为触发,比如登陆、进件申请、还款等,满足一定筛选条件实时给用户发送消息或优惠券对于用户筛选条件,也主要有两种类型用户状态:包括用户自身属性如性别、年龄、学历、收入等,还有用户相关联实体如进件订单、账户信息、还款计划、优惠券等的属性,以及用户画像数据如行为偏好、进件概率等用户行为:即用户的动作,包括登陆、进件申请、还款,甚至前端点击某个按钮、在某个文本框输入都算早期方案早期方案存在以下痛点至少两次跨部门沟通配合成本,周期被拉长非实时消息推送,无法实现基于用户行为的实时推送场景非实时效果验证,无法及时调整运营策略系统搭建的目标需要定义规则,提供可视化界面给业务人员动态配置,无需重启系统即使生效,减少沟通成本和避免重复开发,总之就是要更加 自动化 和 易配置采集实时数据,根据实时事件做实时推送,总之就是要 实时技术选型数据采集、转换、存储采集:状态类的数据主要放在各个业务系统的关系型数据库中,由于历史原因有postgres和mysql,需要实时采集表的数据变更,这里使用kafka connector读取mysql的binlog或postgres的xlog,另外还有标签系统计算出来的标签,在kafka中;而事件类数据主要来源于前端上报事件(有专门的服务接收再丢到kafka),关系型数据库里面也可以提取一些事件。转换:采集出来的数据需要做一些格式统一等操作,用kafka connector。存储:采用Elasticsearch存储用户数据,ES查询不像mysql或mongoDB用B-tree 或B+tree实现索引,而是使用bitset和skip list来处理联合索引,特别适合多字段的复杂查询条件。下面重点看下kafka connector和Elasticsearch如何使用kafka connectorkafka connector有Source和Sink两种组件,Source的作用是读取数据到kafka,这里用开源实现debezium来采集mysql的binlog和postgres的xlog。Sink的作用是从kafka读数据写到目标系统,这里自己研发一套组件,根据配置的规则将数据格式化再同步到ES。kafka connector有以下优点:提供大量开箱即用的插件,比如我们直接用debezium就能解决读取mysql和pg数据变更的问题伸缩性强,对于不同的connector可以配置不同数量的task,分配给不同的worker,,我们可以根据不同topic的流量大小来调节配置。容错性强,worker失败会把task迁移到其它worker上面使用rest接口进行配置,我们可以对其进行包装很方便地实现一套管理界面Elasticsearch对于状态数据,由于状态的写操作相对较少,我们采取嵌套文档的方式,将同个用户的相关实体数据都同步写入到同个文档,具体实现用painless脚本做局部更新操作。效果类似这样:{ “id”:123, “age”:30, “credit_line”:20000, “education”:“bachelor”, … “last_loan_applications”:{ “loan_id”:1234, “status”:“reject”, … } …}事件数据写入比较频繁,数据量比较多,我们使用父子文档的方式做关联,效果类似这样:{ “e_uid”:123, “e_name”:“loan_application”, “e_timestamp”:“2019-01-01 10:10:00” …}(e_前缀是为了防止同个index下同名字段冲突)ES这样存储一方面是方便做统计报表,另一方面跟用户筛选和触达有关。规则引擎在设计规则引擎前,我们对业界已有的规则引擎,主要包括Esper, Drools, Flink CEP,进行了初步调研。EsperEsper设计目标为CEP的轻量级解决方案,可以方便的嵌入服务中,提供CEP功能。优势:轻量级可嵌入开发,常用的CEP功能简单好用。EPL语法与SQL类似,学习成本较低。劣势:单机全内存方案,需要整合其他分布式和存储。以内存实现时间窗功能,无法支持较长跨度的时间窗。无法有效支持定时触达(如用户在浏览发生一段时间后触达条件判断)。DroolsDrools开始于规则引擎,后引入Drools Fusion模块提供CEP的功能。优势:功能较为完善,具有如系统监控、操作平台等功能。规则支持动态更新劣势:以内存实现时间窗功能,无法支持较长跨度的时间窗。无法有效支持定时触达(如用户在浏览发生一段时间后触达条件判断)。FlinkFlink 是一个流式系统,具有高吞吐低延迟的特点,Flink CEP是一套极具通用性、易于使用的实时流式事件处理方案。优势:继承了Flink高吞吐的特点事件支持存储到外部,可以支持较长跨度的时间窗。可以支持定时触达(用followedBy+PartternTimeoutFunction实现)劣势:无法动态更新规则(痛点)自定义规则综上对比了几大开源规则引擎,发现都无法满足业务需求:业务方要求支持长时间窗口(n天甚至n个月,比如放款一个月后如果没产生还款事件就要发消息)动态更新规则,而且要可视化(无论用哪个规则引擎都需要包装,需要考虑二次开发成本)最终我们选择自己根据业务需要,开发基于json的自定义规则,规则类似下面例子:{ “batchId”: “xxxxxxxx”, //流水号,创建每条运营规则时生成 “type”: “trigger”, //usual “triggerEvent”: “login”, “after”: “2h”, //分钟m,小时h,天d,月M “pushRules”: [//支持同时推送多条不同类型的消息 { “pushType”: “sms”, //wx,app,coupon “channel”: “cl”, “content”: “hello #{userInfo.name}” }, { “pushType”: “coupon”, “couponId”: 1234 } ], “statusConditions”: [ { “name”: “and”, //逻辑条件,支持与(and)或(or)非(not) “conditions”: [ { “name”: “range”, “field”: “credit_line”, “left”: 2000, “right”: 10000, “includeLeft”: true, “includeRight”: false }, { “name”:“in”, “filed”:“education”, “values”:[“bachelor”,“master”] } ] } ], “eventConditions”: [ { “name”: “or”,//逻辑条件,支持与(and)或(or)非(not) “conditions”: [ { “name”: “event”, “function”: “count”, //聚合函数,目前只支持count “eventName”: “xxx_button_click”, “range”: { //聚合结果做判断 “left”: 1, “includeLeft”: true }, “timeWindow”: { “type”: “fixed”, //fixed为固定窗口,sliding为滑动窗口 “start”: “2019-01-01 01:01:01”, “end”: “2019-02-01 01:01:01” }, “conditions”: [ //event查询条件继承and逻辑条件,所以事件也可以过滤字段 { “name”: “equals”, “field”: “f1”, “value”: “v1” } ] } ] } ]}使用面向对象思维对过滤条件做抽象后,过滤条件继承关系如下:然后代码里加一层parser把Condition都转成ES查询语句,实现轻量级的业务规则配置功能。整体技术方案系统组成模块及功能如下:mysql binlog:mysql的数据变更,由kafka connector插件读取到kafka,数据源之一postgres xlog:pg的数据变更,由kafka connector插件读取到kafka,数据源之一report server:事件上报服务,数据源之一tags:用户画像系统计算出来的标签,数据源之一触发场景路由:分实时触发和延迟触发,实时触发直接到下一步,延迟触发基于 rabbitmq的延迟队列实现用户筛选模块:将筛选规则翻译为ES查询语句到ES查询用户数据,可以是批量的和单个用户的变量渲染模块:对推送内容做处理推送适配器:兼容不同的推送方式定时任务调度器:基于elastic-job,处理定时推送任务规则配置控制台:提供可视化配置界面(运营规则配置、数据采集规则配置、字段元数据配置等)报表服务:提供报表查询功能运营位服务:提供外部接口,根据条件匹配运营位(如启动图、首页banner图片等)总结与展望系统基本满足了目前的业务需求,对转化率等运营指标提升显著可以扩展其它业务,如推荐、风控、业务监控等规则定时拉取,实时性差,可以用zk做发布订阅实现即时更新目前事件的聚合函数只支持count,能满足业务需求但是未来可能还需要支持其它函数系统只经过千万级用户的生产验证,再高数量级的话可能还有很多性能优化的工作,如ES并行查询(目前用scroll api批量拉取用户数据是串行的)事件类数据越来越多,目前采取定时删除半年前数据的方式,防止持续增长过快不可控,所以事件类条件不可超过半年的时间窗口虽然系统对业务无入侵,但是反过来看本系统依赖于上游数据,上游数据发生变化时如何做到影响最小?未来会继续从技术及业务两方面入手,将系统建设的更加易用、高效。 ...

February 17, 2019 · 1 min · jiezi

设计一个分布式RPC框架

0 前言提前先祝大家春节快乐!好了,先简单聊聊。我从事的是大数据开发相关的工作,主要负责的是大数据计算这块的内容。最近Hive集群跑任务总是会出现Thrift连接HS2相关问题,研究了解了下内部原理,突然来了兴趣,就想着自己也实现一个RPC框架,这样可以让自己在设计与实现RPC框架过程中,也能从中了解和解决一些问题,进而让自己能够更好的发展(哈哈,会不会说我有些剑走偏锋?不去解决问题,居然研究RPC。别急,这类问题已经解决了,后续我也会发文章详述的)。1 RPC流水线工程?原理图上我已经标出来流程序号,我们来走一遍:① Client以本地调用的方式调用服务② Client Stub接收到调用后,把服务调用相关信息组装成需要网络传输的消息体,并找到服务地址(host:port),对消息进行编码后交给Connector进行发送③ Connector通过网络通道发送消息给Acceptor④ Acceptor接收到消息后交给Server Stub⑤ Server Stub对消息进行解码,并根据解码的结果通过反射调用本地服务⑥ Server执行本地服务并返回结果给Server Stub⑦ Server Stub对返回结果组装打包并编码后交给Acceptor进行发送⑧ Acceptor通过网络通道发送消息给Connector⑨ Connector接收到消息后交给Client Stub,Client Stub接收到消息并进行解码后转交给Client⑩ Client获取到服务调用的最终结果由此可见,主要需要RPC负责的是2~9这些步骤,也就是说,RPC主要职责就是把这些步骤封装起来,对用户透明,让用户像调用本地服务一样去使用。2 为RPC做个技术选型序列化/反序列化首先排除Java的ObjectInputStream和ObjectOutputStream,因为不仅需要保证需要序列化或反序列化的类实现Serializable接口,还要保证JDK版本一致,公司应用So Many,使用的语言也众多,这显然是不可行的,考虑再三,决定采用Objesess。通信技术同样我们首先排除Java的原生IO,因为进行消息读取的时候需要进行大量控制,如此晦涩难用,正好近段时间也一直在接触Netty相关技术,就不再纠结,直接命中Netty。高并发技术远程调用技术一定会是多线程的,只有这样才能满足多个并发的处理请求。这个可以采用JDK提供的Executor。服务注册与发现Zookeeper。当Server启动后,自动注册服务信息(包括host,port,还有nettyPort)到ZK中;当Client启动后,自动订阅获取需要远程调用的服务信息列表到本地缓存中。负载均衡分布式系统都离不开负载均衡算法,好的负载均衡算法可以充分利用好不同服务器的计算资源,提高系统的并发量和运算能力。非侵入式借助于Spring框架RPC架构图如下:3 让RPC梦想成真由架构图,我们知道RPC是C/S结构的。3.1 先来一个单机版单机版的话比较简单,不需要考虑负载均衡(也就没有zookeeper),会简单很多,但是只能用于本地测试使用。而RPC整体的思想是:为客户端创建服务代理类,然后构建客户端和服务端的通信通道以便于传输数据,服务端的话,就需要在接收到数据后,通过反射机制调用本地服务获取结果,继续通过通信通道返回给客户端,直到客户端获取到数据,这就是一次完整的RPC调用。3.1.1 创建服务代理可以采用JDK原生的Proxy.newProxyInstance和InvocationHandler创建一个代理类。详细细节网上博客众多,就不展开介绍了。当然,也可以采用CGLIB字节码技术实现。3.1.2 构建通信通道 & 消息的发送与接收客户端通过Socket和服务端建立通信通道,保持连接。可以通过构建好的Socket获取ObjectInputStream和ObjectOutputStream。但是有一点需要注意,如果Client端先获取ObjectOutputStream,那么服务端只能先获取ObjectInputStream,不然就会出现死锁一直无法通信的。3.1.3 反射调用本地服务服务端根据请求各项信息,获取Method,在Service实例上反向调用该方法。3.2 再来一个分布式版本我们先从顶层架构来进行设计实现,也就是技术选型后的RPC架构图。主要涉及了借助于,Zookeeper实现的服务注册于发现。3.2.1 服务注册与发现当Server端启动后,自动将当前Server所提供的所有带有@ZnsService注解的Service Impl注册到Zookeeper中,在Zookeeper中存储数据结构为 ip:httpPort:acceptorPort当Client端启动后,根据扫描到的带有@ZnsClient注解的Service Interface从Zookeeper中拉去Service提供者信息并缓存到本地,同时在Zookeeper上添加这些服务的监听事件,一旦有节点发生变动(上线/下线),就会立即更新本地缓存。3.2.2 服务调用的负载均衡Client拉取到服务信息列表后,每个Service服务都对应一个地址list,所以针对连哪个server去调用服务,就需要设计一个负载均衡路由算法。当然,负载均衡算法的好坏,会关系到服务器计算资源、并发量和运算能力。不过,目前开发的RPC框架zns中只内置了Random算法,后续会继续补充完善。3.2.3 网络通道Acceptor当Server端启动后,将同时启动一个Acceptor长连接线程,用于接收外部服务调用请求。内部包含了编解码以及反射调用本地服务机制。Connector当Client端发起一个远程服务调用时,ZnsRequestManager将会启动一个Connector与Acceptor进行连接,同时会保存通道信息ChannelHolder到内部,直到请求完成,再进行通道信息销毁。3.2.4 请求池管理为了保证一定的请求并发,所以对服务调用请求进行了池化管理,这样可以等到消息返回再进行处理,不需要阻塞等待。3.2.5 响应结果异步回调当Client端接收到远程服务调用返回的结果时,直接通知请求池进行处理,No care anything!4. 总结本次纯属是在解决Thrift连接HS2问题时,突然来了兴趣,就构思了几天RPC大概架构设计情况,便开始每天晚上疯狂敲代码实现。我把这个RPC框架命名为zns,现在已经完成了1.0-SNAPSHOT版本,可以正常使用了。在开发过程中,也遇到了一些平时忽略的小问题,还有些是工作工程中没有遇到或者遗漏的地方。因为是初期,所以会存在一些bug,如果你感兴趣的话,欢迎提PR和ISSUE,当然也欢迎把代码clone到本地研究学习。虽然就目前来看,想要做成一个真正稳定可投产使用的RPC框架还有短距离,但是我会坚持继续下去,毕竟RPC真的涉及到了很多点,只有真正开始做了,才能切身体会和感受到。Ya hoh!终于成功实现了v1.0,嘿嘿……源码地址zns源码地址zns源码简单介绍:zns由zns-api, zns-common, zns-client, zns-server四个核心模块组成。zns-service-api, zns-service-consumer, zns-service-provider三个模块是对zns进行测试使用的案例。

February 2, 2019 · 1 min · jiezi

架构设计步骤

本文是对极客时间专栏《从零开始学架构》部分内容的总结。架构设计步骤分为如下四步:识别复杂度,设计备选方案,方案选型和详细设计。具体操作如下:首先是识别系统复杂度。复杂度包括高性能、高可用、可扩展、低成本、安全和规模几个方面。识别时,可以采用排除法依次进行分析。如果复杂度有多个,需要综合业务、技术、团队等情况进行排序,优先解决当前面临的最主要的复杂度问题。识别到复杂度后,进行备选方案的设计。一般选取3-5个方案最佳;每个方案之间要有明显差异;选择备选方案时,要综合考察各种技术,不能只选熟悉的技术;另外要注意不要太过深入细节。然后是方案选型。方案的选型采取“360度环评",即识别出方案的质量属性点,根据业务、技术、团队等情况进行优先级排序,首先选择满足第一优先级的,如果都满足,再看第二优先级的,依次类推。常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。最后是详细设计,将方案细节敲定落地。在这个过程中,为了防止方案无法落地,需在前面2步提前做一些工作,包括将大方案拆解成小方案,多人共同设计避免盲区等。

February 2, 2019 · 1 min · jiezi

题库分库分表架构方案

个人博客地址 https://www.texixi.com/2019/0…方案项目背景在现在题库架构下,针对新购买的1300W多道数据进行整合,不影响现有功能。由于数据量偏多,需要进行数据的切分目标场景兼容旧的功能对1300多W数据进行分库分表需要对旧的数据进行整合老师端选题组卷 可以根据 学段、学科、知识点、难度、题型 来筛选学生端根据老师端所选题目获取对应的题目对3年内以后扩展的增量数据预留数量空间数据样例学段数据量小学1285336初中6655780高中6144072学段学科数据量初中数学1869524初中化学1356224初中英语288440切分方案一切分为3个库, 分别是小学、初中、高中 数据占比如上每个库切分10个表 根据 (学科+首级知识点)%10每个库一个总表缺点:例:用到不同知识点时,需要多表获取数据优点:数据分布较为平均切分方案二 (采用)切分为3个库, 分别是小学、初中、高中 数据占比如上每个库切分10个表(全部10个学科) 根据 学科区分, 例: 数学表、物理表每个库一个总表缺点:数据不大平均, 数据量多的例数学有186W多、英语28W多优点:当有用到组卷等需要筛选多知识点题目时,不用多表查询数据id 自增区间划分小学 1-2亿中学 2-3亿高中 3亿起关联关系图根据知识点获取题目流程自增id对原有的id区间段不做处理对切分后的id自增段进行规划兼容旧功能解决的问题新旧数据有重复的知识点、题目新旧数据的结构不一样对旧的题库功能代码的修改两套题库合并主键冲突问题兼容旧功能 方案一 (个人推荐)有操作的旧的数据洗入新的结构,旧的数据只为兼容原有的功能数据,不做显示。优点:不用变动数据结构,最新的购买的数据结构较为清晰。 易维护扩展,因为目前旧的数据已经整合了两套数据缺点:需要修改全部旧有的功能代码(针对新的数据结构)兼容旧功能 方案二把新购买的数据整合进老的数据结构,同时保留三批数据,需要处理所有表的主键冲突、三批各表数据去重优点:旧有代码只修改数据结构切分的部分,不用全部修改功能代码缺点:数据较乱,三套不同的数据同时存在数据库需要处理新的结构整合进旧的数据结构,同时需要处理主键冲突,代码上需要处理对应的数据问题点测试环境和正式环境图片存放在那里?100多G,上传cdn需要几十天时间,有4000多W张,目前cdn不支持打包上传解决方案:购买单独服务器,主备,存放图片测试db 正式db 1300多w 目前占用100G左右, 需要存放空间解决方案:测试环境新加硬盘,新加db实例端口3307,正式环境db存放在图片服务器代码设计模式采用适配器模式(原先的代码结构不变)类图调研内容中间件MYCAT(未使用)什么是MYCAT一个彻底开源的,面向企业应用开发的大数据库集群支持事务、ACID、可以替代MySQL的加强版数据库一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品一个新颖的数据库中间件产品MYCAT特性==支持库内分表(1.6)====支持单库内部任意join,支持跨库2表join,甚至基于caltlet的多表join==支持全局序列号,解决分布式下的主键生成问题。==分片规则丰富==,插件化开发,易于扩展。基于Nio实现,有效管理线程,解决高并发问题。==支持通过全局表,ER关系的分片策略,实现了高效的多表join查询==支持分布式事务(弱xa)。支持SQL黑名单、sql注入攻击拦截==支持MySQL、Oracle、DB2、SQL Server、PostgreSQL等DB的常见SQL语法====遵守Mysql原生协议==,跨语言,跨平台,跨数据库的通用中间件代理。==基于心跳的自动故障切换,支持读写分离,支持MySQL主从,==以及galera cluster集群。可以大幅降低开发难度,提升开发速度具体看 mycat 官网Mycat 注意事项全局表一致性检测 1.6版本开始支持(一致性的定时检测)分片 join(尽量避免使用 Left join 或 Right join,而用 Inner join)Mycat 原理应用要面对很多个数据库的时候,这个时候就需要对数据库层做一个抽象,来管理这些数据库,而最上面的应用只需要面对一个数据库层的抽象或者说数据库中间件就好了,这就是Mycat的核心作用。分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。Mycat 应用场景读写分离,配置简单分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片报表系统,借助于Mycat的分表能力,处理大规模报表的统计文章整理应用场景 那些适合,那些不适合 https://www.cnblogs.com/barry…使用说明 https://juejin.im/post/59c325…总表使用mysql MERGE 引擎(不考虑)合并的表使用的必须是MyISAM引擎表的结构必须一致,包括索引、字段类型、引擎和字符集对于增删改查,直接操作总表即可。数据切分原则能不切分尽量不要切分。如果要切分一定要选择合适的切分规则,提前规划好。数据切分尽量通过数据冗余或表分组(Table Group)来降低跨库 Join 的可能。由于数据库中间件对数据 Join 实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表 Join。尽可能的比较均匀分布数据到各个节点上该业务字段是最频繁的或者最重要的查询条件。

January 29, 2019 · 1 min · jiezi

关于公司架构管控的思考

假想背景:现状是,各子系统的新建及重大迭代都会形式化地走架构审批流程,但应用架构是否设计以及是否合理,信息技术部门不能掌握。而架构规划部门的架构师人屈指可数,面对总人数达数百人的开发团队所负责的几十子系统、每个月数十个迭代特性,无法做到直接帮助开发团队详尽的进行架构设计。由此提出:架构审批流程不代表架构设计、架构规划部门要加强架构管控。要做好架构管控,需要能够回答几个问题:架构管控的目的是什么?架构管控的目标是什么?架构管控需要管控什么内容?如何进行管控?一、目的一个稳定发展、创新发展的企业,支援业务发展的信息系统的稳定与效率同等重要。架构管控的目的,要指导各团队技术负责人设计出合理的系统,能够满足稳定与开发效率的要求,能够满足功能与非功能的需求,能够考虑到各级相关者的意见;并且还要有考察开发过程及运营过程的机制,以确定最终交付的软件系统复合架构设计及开展架构设计的持续改善工作。二、制定目标为了达到这些我们架构管控的目标又是什么呢?发布一份指导意见?发布一份制度说明?发布一份评分表?我认为这些应该归属于如何进行管控,而不是目标。我们需要梳理现状,找出主要矛盾,量化主要矛盾,形成目标(SMART原则别忘记~~)(一)、主要生产问题、客户投诉及分类从客户反馈中可以抓住主要矛盾。(二)、信息技术方面的主要矛盾外购产品 vs 二次改造早期从外部采购的产品一般会使用比较老旧的开发框架,进行二次改造困难,常带来较多故障,并且开发效率低下。系统解耦 vs 互相影响分布式带来较好的伸缩性等非功能特性的同时,也带来了复杂度 —— 系统间相互影响较难控制,给设计和开发增大了难度。开发团队对科技公共平台的不熟悉 vs 强制使用业务数据模型混乱 vs 新需求5、控制 vs 创新(三)、形成架构管控目标控制新增外购系统的架构方案的合理性、梳理既存外购系统的架构、提升开发效率。降低分布式系统的开发难度、提升分布式系统的交付质量。降低科技公共平台的使用难度。引导、规范新技术的引入。

January 22, 2019 · 1 min · jiezi

阿里巴巴是如何打通 CMDB,实现就近访问的?

CMDB在企业中,一般用于存放与机器设备、应用、服务等相关的元数据。当企业的机器及应用达到一定规模后就需要这样一个系统来存储和管理它们的元数据。有一些广泛使用的属性,例如机器的IP、主机名、机房、应用、region等,这些数据一般会在机器部署时录入到CMDB,运维或者监控平台会使用这些数据进行展示或者相关的运维操作。在服务进行多机房或者多地域部署时,跨地域的服务访问往往延迟较高,一个城市内的机房间的典型网络延迟在1ms左右,而跨城市的网络延迟,例如上海到北京大概为30ms。此时自然而然的一个想法就是能不能让服务消费者和服务提供者进行同地域访问。我们在集团内部的实践中,这样的需求是通过和CMDB打通来实现的。Nacos的服务发现组件中,对接CMDB,然后通过配置的访问规则,来实现服务消费者到服务提供者的同地域优先。<div data-type=“alignment” data-value=“center” style=“text-align:center”> <div data-type=“p”>图1 服务的同地域优先访问</div></div>这实际上就是一种负载均衡策略,在Nacos的规划中,丰富的服务端的可配置负载均衡策略是我们的重要发展方向,这与当前已有的注册中心产品不太一样。在设计如何在开源的场景中,支持就近访问的时候,与企业自带的CMDB集成是我们考虑的一个核心问题。除此之外,我们也在考虑将Nacos自身扩展为一个实现基础功能的CMDB。无论如何,我们都需要能够从某个地方获取IP的环境信息,这些信息要么是从企业的CMDB中查询而来,要么是从自己内置的存储中查询而来。CMDB插件机制先不考虑如何将CMDB的数据应用于负载均衡,我们需要首先在Nacos里将CMDB的数据通过某种方法获取。在实际使用中,基本上每个公司都会通过购买或者自研搭建自己的CMDB,那么为了能够解耦各个企业的CMDB具体实现,一个比较好的策略是使用SPI机制,约定CMDB的抽象调用接口,由各个企业添加自己的CMDB插件,无需任何代码上的重新构建,即可在运行状态下对接上企业的CMDB。<div data-type=“alignment” data-value=“center” style=“text-align:center”> <div data-type=“p”>图2 Nacos CMDB SPI机制原理</div></div>如图2所示,Nacos定义了一个SPI接口,里面包含了与第三方CMDB约定的一些方法。用户依照约定实现了相应的SPI接口后,将实现打成jar包放置到Nacos安装目录下,重启Nacos即可让Nacos与CMDB的数据打通。整个流程并不复杂,但是理解CMDB SPI接口里方法和相应概念的含义不太简单。在这里对CMDB机制的相关概念和接口含义做一个详细说明。CMDB抽象概念实体(Entity)实体是作为CMDB里数据的承载方,在一般的CMDB中,一个实体可以指一个IP、应用或者服务。而这个实体会有很多属性,例如IP的机房信息,服务的版本信息等。实体类型(Entity Type)我们并不限定实体一定是IP、应用或者服务,这取决于实际的业务场景。Nacos有计划在未来支持不同的实体类型,不过就目前来说,服务发现需要的实体类型是IP。标签(Label)Label是我们抽象出的Entity属性,Label定义为一个描述Entity属性的K-V键值对。Label的key和value的取值范围一般都是预先定义好的,当需要对Label进行变更,如增加新的key或者value时,需要调用单独的接口并触发相应的事件。一个常见的Label的例子是IP的机房信息,我们认为机房(site)是Label的key,而机房的集合(site1, site2, site3)是Label的value,这个Label的定义就是:site: {site1, site2, site3}。实体事件(Entity Event)实体的标签的变更事件。当CMDB的实体属性发生变化,需要有一个事件机制来通知所有订阅方。为了保证实体事件携带的变更信息是最新准确的,这个事件里只会包含变更的实体的标识以及变更事件的类型,不会包含变更的标签的值。CMDB约定接口在设计与CMDB交互接口的时候,我们参考了内部对CMDB的访问接口,并与若干个外部客户进行了讨论。我们最终确定了以下要求第三方CMDB插件必须实现的接口:获取标签列表Set<String> getLabelNames();这个方法将返回CMDB中需要被Nacos识别的标签名集合,CMDB插件可以按需决定返回什么标签个Nacos。不在这个集合的标签将会被Nacos忽略,即使这个标签出现在实体的属性里。我们允许这个集合会在运行时动态变化,Nacos会定时去调用这个接口刷新标签集合。获取实体类型Set<String> getEntityTypes();获取CMDB里的实体的类型集合,不在这个集合的实体类型会被Nacos忽略。服务发现模块目前需要的实体类似是ip,如果想要通过打通CMDB数据来实现服务的高级负载均衡,请务必在返回集合里包含“ip”。获取标签详情Label getLabel(String labelName);获取标签的详细信息。返回的Label类里包含标签的名字和标签值的集合。如果某个实体的这个标签的值不在标签值集合里,将会被视为无效。查询实体的标签值String getLabelValue(String entityName, String entityType, String labelName);Map<String, String> getLabelValues(String entityName, String entityType);这里包含两个方法,一个是获取实体某一个标签名对应的值,一个是获取实体所有标签的键值对。参数里包含实体的值和实体的类型。注意,这个方法并不会在每次在Nacos内部触发查询时去调用,Nacos内部有一个CMDB数据的缓存,只有当这个缓存失效或者不存在时,才会去访问CMDB插件查询数据。为了让CMDB插件的实现尽量简单,我们在Nacos内部实现了相应的缓存和刷新逻辑。查询实体Map<String, Map<String, Entity>> getAllEntities();Entity getEntity(String entityName, String entityType);查询实体包含两个方法:查询所有实体和查询单个实体。查询单个实体目前其实就是查询这个实体的所有标签,不过我们将这个方法与获取所有标签的方法区分开来,因为查询单个实体方法后面可能会进行扩展,比查询所有标签获取的信息要更多。查询所有实体则是一次性将CMDB的所有数据拉取过来,该方法可能会比较消耗性能,无论是对于Nacos还是CMDB。Nacos内部调用该方法的策略是通过可配置的定时任务周期来定时拉取所有数据,在实现该CMDB插件时,也请关注CMDB服务本身的性能,采取合适的策略。查询实体事件List<EntityEvent> getEntityEvents(long timestamp);这个方法意在获取最近一段时间内实体的变更消息,增量的去拉取变更的实体。因为Nacos不会实时去访问CMDB插件查询实体,需要这个拉取事件的方法来获取实体的更新。参数里的timestamp为上一次拉取事件的时间,CMDB插件可以选择使用或者忽略这个参数。CMDB插件开发流程参考 https://github.com/nacos-group/nacos-examples,这里已经给出了一个示例plugin实现。具体步骤如下:新建一个maven工程,引入依赖nacos-api: <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-api</artifactId> <version>0.7.0</version> </dependency>引入打包插件: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin>定义实现类,继承com.alibaba.nacos.api.cmdb.CmdbService,并实现相关方法。在src/main/resource/目录下新建目录:META-INF/services在src/main/resources/META-INF/services目录下新建文件com.alibaba.nacos.api.cmdb.CmdbService,并在文件里将第三步中创建的实现类全名写入该文件:代码自测完成后,执行命令进行打包:mvn package assembly:single -Dmaven.test.skip=true将target目录下的包含依赖的jar包上传到nacos CMDB插件目录:{nacos.home}/plugins/cmdb在nacos的application.properties里打开加载插件开关:nacos.cmdb.loadDataAtStart=true重启nacos Server,即可加载到您实现的nacos-cmdb插件获取您的CMDB数据。使用Selector实现同机房优先访问在拿到CMDB的数据之后,就可以运用CMDB数据的强大威力来实现多种灵活的负载均衡策略了,下面举例来说明如何使用CMDB数据和Selector来实现就近访问。假设目前Nacos已经通过CMDB拿到了一些IP的机房信息,且它们对应的标签信息如下:11.11.11.11 site: x1122.22.22.22 site: x1233.33.33.33 site: x1144.44.44.44 site: x1255.55.55.55 site: x1311.11.11.11、22.22.22.22、33.33.33.33、44.44.44.44和55.55.55.55.55都包含了标签site,且它们对应的值分别为x11、x12、x11、x12、x13。我们先注册一个服务,下面挂载IP11.11.11.11和22.22.22.22。<div data-type=“alignment” data-value=“center” style=“text-align:center”> <div data-type=“p”>图3 服务详情</div></div>然后我们修改服务的“服务路由类型”,并配置为基于同site优先的服务路由:<div data-type=“alignment” data-value=“center” style=“text-align:center”> <div data-type=“p”>图4 编辑服务路由类型</div></div>这里我们将服务路由类型选择为标签,然后输入标签的表达式:CONSUMER.label.site = PROVIDER.label.site这个表达式的格式和我们抽象的Selector机制有关,具体将会在另外一篇文章中介绍。在这里您需要记住的就是,任何一个如下格式的表达式:CONSUMER.label.labelName = PROVIDER.label.labelName将能够实现基于同labelName优先的负载均衡策略。然后假设服务消费者的IP分别为33.33.33.33、44.44.44.44和55.55.55.55,它们在使用如下接口查询服务实例列表:naming.selectInstances(“nacos.test.1”, true)那么不同的消费者,将获取到不同的实例列表。33.33.33.33获取到11.11.11.11,44.44.44.44将获取到22.22.22.22,而55.55.55.55将同时获取到11.11.11.11和22.22.22.22。以上,便是我们在Nacos中通过打通CMDB,实现就近访问的实践。Nacos是阿里巴巴开源的服务注册与配置管理产品,参考:《阿里启动新项目:Nacos,比 Eureka 更强!》。本文原创首发于微信公众号:Java技术栈(id:javastack),关注公众号在后台回复 “架构” 可获取更多,转载请原样保留本信息。 ...

January 11, 2019 · 1 min · jiezi

微服务架构:如何用十步解耦你的系统?

导言:耦合性,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。高内聚低耦合,是软件工程中的概念,是判断设计好坏的标准,主要是面向对象的设计,主要是看类的内聚性是否高,耦合度是否低。SpringCloud和Dubbo都是现在比较成熟的微服务框架,如何使用两者构建搭建你的微服务系统呢?他们是如何将你的系统解耦的?又是怎么解耦的呢?请听我慢慢道来:第一步,解耦现有模块将现有耦合在一起的模块进行重新的设计,设计成可以独立部署的多个模块,使用微服务框架很容易做到,成熟的示例代码都特别多,这里不再多讲。下面是我的微服务实现的一个架构设计图。第二步,抽取公共模块架构设计原则之一就是反向依赖,只从上往下依赖,所以,我们将公共的重复功能的模块抽取出来。必须强调一点的是,公共模块必须足够的功能单一,不能有其他业务的逻辑判断在里面。在整个模块依赖关系里,应该是一棵树状结构的关系图,而不是一个网状的关系图。1)做好代码控制笔者之前就碰到过这种问题,模块划分完了,当需求变更的时候,研发人员根本不管是不是公共模块,只要能快速完成任务,哪里改的快就在哪里改。因此,这个需要内部要做好代码的权限管理,不应该开放所有的提交代码的权限给所有的人。后来我就将公共模块的合并代码的权限收回了,合并代码需要先提交申请,代码review过才能合并代码。这就保证了公共模块代码的功能单一。2)做好版本管理公共模块被多个模块模块使用,任何代码的修改都可能会导致到正在使用的模块无法使用。这个就需要做好各个模块的版本管理,我是使用maven进行版本管理的,定义一个总的父pom项目来进行各个模块的版本管理,任何被其他模块使用的开发包都要在父pom里进行版本管理。当新的需求来了以后,需要对公共模块进行修改时,要更新模块的版本号,同时更新父pom的版本号,需要使用公共模块新功能的模块就修改父pom的版本号,不需要使用公共模块新功能的模块就不用修改父pom的版本号,这样公共模块的新老版本都能使用,即使出现问题,也只会影响到使用新版本的模块。第三步,解耦迭代需求现在的代码迭代速度快,同时会面对多个需求,有的需求紧急,有的需求不紧急,而且紧急程度可能随时会调整,如果将所有的需求都放在一个分支,当只想上线其中几个需求的时候发现无法将不上线需求的代码拆分出来,是不是很尴尬,即使能拆分出来,代码修改过以后又要重新进行部署测试,很费时费力,所以要针对不同的需求重新建立研发分支,这样就将不同需求的分支解耦,保证想上哪个就上哪个,需要上多个需求的就将分支合并上线。第四步,配置解耦为每个模块每个环境配置一个配置文件,这样就可以把不同的环境的配置解耦,不用每次上线都更新一次。但是如果需要修改数据库配置,还是需要重新部署重启应用才能解决。使用微服务的配置中心就能解决这个问题了,比如使用ZooKeeper作为SpringCloud的配置中心,修改ZooKeeper中的节点数据就可以实时更新配置并生效。第五步,权限解耦当采用微服务架构把原来的系统拆分成多个系统以后,你会发现原来简单的问题,现在变的复杂了,比如功能的权限控制,原来是跟业务代码放到一起,现在如果每个业务模块都有功能权限的代码,将是一件非常麻烦的事情。那么解决办法就是将权限功能迁移出来,恰巧使用SpringCloudGateway就能完成这件事情,SpringCloudGateway能够进行负载均衡,各种路由拦截,只要将原来的权限控制代码迁移到Gateway里实现以下就可以了,权限配置管理界面和代码逻辑都不用变。如果是API接口呢,就需要将安全验证等功能放在Gateway里实现就好了。第六步,流量解耦当你的系统访问量越来越大的时候,你会发现每次升级都是一件非常麻烦的事情,领导会跟你说这个功能忙时不能停机影响用户使用呀,只能半夜升级呀,多么痛快的事情啊。有的时候运营人员也会发现,怎么我的后台访问怎么这么慢?问题出在哪里呢?问题就出在,所有的模块都用了一个Gateway,多端同时使用了相同的流量入口,当在举行大促时,并发量非常高,带宽占用非常大,那么其他的功能也会跟着慢下来。不能在举行大促时发券时,我线下支付一直支付不了,这是非常严重的事故了,客服电话会被打爆了。所以,必须要对流量进行拆分,各个端的流量不能相互影响,比如APP端、微信端、运营后台和商户后台等都要分配独立的Gateway,并接入独立的带宽,对于流量大的端可以使用弹性带宽,对于运营后台和商户后台就比较小的固定的带宽即可。这样就大大降低了升级时的难度,是不是再上线时就没那么紧张了?第七步,数据解耦系统刚上线的时候,数据量不大,所有的模块感觉都挺好的,当时间一长,系统访问量非常大的时候会发现功能怎么都变慢了,怎么mysql的cpu经常100%。那么恭喜你,你中招了,你的数据需要解耦了。首先要模块间数据解耦,将不同模块使用独立的数据库,保证各模块之间的数据不相互影响。其次就是冷热数据解耦,同一个模块运行时间长了以后也会积累大量的数据,为了保证系统的性能的稳定,要减少因为数据量太大造成的性能降低,需要对历史数据进行定期的迁移,对于完整数据分析汇总就在其他的库中实现。第八步,扩容解耦一个好的架构设计是要有好的横向扩展的能力,在不需要修改代码只通过增加硬件的方式就能提高系统的性能。SpringCloud和Dubbo的注册中心天生就能够实现动态添加模块的节点,其他模块调用能够实时发现并请求到新的模块节点上。第九步,部署解耦互联网开发在于能够快速的试错,当一个新的版本上线时,经常是需要先让一部分用户进行测试一下,这就是传说中的灰度发布,同一个模块先部署升级几台服务器到新版本,重启完成后流量进来以后,就可以验证当前部署的这几台服务器有没有问题,就继续部署其他的节点,如果有问题马上回滚到上一个版本。使用SpringCloudGateway的WeighRouterFilter就能实现这个功能。第十步,动静解耦当同一个模块的瞬间有非常高并发的时候,对,就是说的秒杀,纯粹的流量解耦还是不够,因为不能让前面的流量冲击后面真正的下单的功能,这个时候就需要更细的流量解耦,要将静态文件的访问通通抛给CDN来解决,动态和静态之间是通过定时器来出发的,时间未到之前一直刷新的是静态的文件,当时间到了之后,生成新的js文件,告诉静态页面可以访问下单功能了。总结在模块划分时,要遵循“一个模块,一个功能”的原则,尽可能使模块达到功能内聚。事实上,微服务架构短期来看,并没有很明显的好处,甚至短期内会影响系统的开发进度,因为高内聚,低耦合的系统对开发设计人员提出了更高的要求。高内聚,低耦合的好处体现在系统持续发展的过程中,高内聚,低耦合的系统具有更好的重用性,维护性,扩展性,可以更高效的完成系统的维护开发,持续的支持业务的发展,而不会成为业务发展的障碍。———— / END / ————

December 4, 2018 · 1 min · jiezi

前端架构设计的方法论

前端架构设计的方法论系统的架构设计用来定义应用程序的基本特征和行为。良好的架构是系统构建成功的关键。架构驱动的软件开发是构建复杂系统的最有效方法,架构驱动的方法优于需求驱动,文档驱动和方法论(抽象推理的能力)驱动。虽然方法论(抽象推理的能力)可以帮助我们取得项目的成功,但是它并不是决定性的因素。1、初期如何设计架构所有架构的核心:关注点分离(分离角色和职能,分离之后的结果是对具体功能的高度抽象)。架构设计的过程其实也是在梳理需求的过程中不断标识、封装和操纵关注点。根据迪米特法则和开闭原则,分离之后的职责对象应该高度独立和封闭(优点是不需要关系它们内部的具体实现,只关心输入和输出即可)。更容易构造有效的(职责)角色和强力的模型,变的更好开发,测试,管理和维护。2、构建系统的步骤1、抽象职责(功能模块)之间的相互作用2、抽象职责和数据流之间的关系3、注意的四个点1、扩展性2、弹性(伸缩性)3、灵活性4、稳定性4、评判标准1、灵活性响应外部环境变化的能力,架构中是否便捷做一些改变,功能模块间的紧耦合是降低灵活性的关键。2、易于部署3、易于开发4、可测试性职责和数据流的划分,便于分块测试。5、伸缩性系统是否利于扩展,紧耦合与职责划分不清晰是降低伸缩性的关键。6、性能任何架构的本质是在处理数据流,所以数据流的流转效率决定了该架构的性能。最后本文提出的这些观点实际上也是属于架构设计的方法论。在掌握并熟练运用了这些方法论之后并实践到项目中,慢慢的才会搭建出更好的架构。ps:由于本人比较懒,所以没有针对一些名词做具体讲解和示例。

November 1, 2018 · 1 min · jiezi

当深度学习遇见自动文本摘要

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由columneditor发表于云+社区专栏作者:姚均霖导语 :随着近几年文本信息的爆发式增长,人们每天能接触到海量的文本信息,如新闻、博客、聊天、报告、论文、微博等。从大量文本信息中提取重要的内容,已成为我们的一个迫切需求,而自动文本摘要(automatic text summarization)则提供了一个高效的解决方案。介绍随着近几年文本信息的爆发式增长,人们每天能接触到海量的文本信息,如新闻、博客、聊天、报告、论文、微博等。从大量文本信息中提取重要的内容,已成为我们的一个迫切需求,而自动文本摘要(automatic text summarization)则提供了一个高效的解决方案。根据Radev的定义[3],摘要是“一段从一份或多份文本中提取出来的文字,它包含了原文本中的重要信息,其长度不超过或远少于原文本的一半”。自动文本摘要旨在通过机器自动输出简洁、流畅、保留关键信息的摘要。自动文本摘要有非常多的应用场景,如自动报告生成、新闻标题生成、搜索结果预览等。此外,自动文本摘要也可以为下游任务提供支持。尽管对自动文本摘要有庞大的需求,这个领域的发展却比较缓慢。对计算机而言,生成摘要是一件很有挑战性的任务。从一份或多份文本生成一份合格摘要,要求计算机在阅读原文本后理解其内容,并根据轻重缓急对内容进行取舍,裁剪和拼接内容,最后生成流畅的短文本。因此,自动文本摘要需要依靠自然语言处理/理解的相关理论,是近几年来的重要研究方向之一。自动文本摘要通常可分为两类,分别是抽取式(extractive)和生成式(abstractive)。抽取式摘要判断原文本中重要的句子,抽取这些句子成为一篇摘要。而生成式方法则应用先进的自然语言处理的算法,通过转述、同义替换、句子缩写等技术,生成更凝练简洁的摘要。比起抽取式,生成式更接近人进行摘要的过程。历史上,抽取式的效果通常优于生成式。伴随深度神经网络的兴起和研究,基于神经网络的生成式文本摘要得到快速发展,并取得了不错的成绩。本文主要介绍基于深度神经网络的生成式自动文本摘要,着重讨论典型的摘要模型,并介绍如何评价自动生成的摘要。对抽取式和不基于深度神经网络的生成式自动文本摘要感兴趣的同学可以参考1。生成式文本摘要生成式文本摘要以一种更接近于人的方式生成摘要,这就要求生成式模型有更强的表征、理解、生成文本的能力。传统方法很难实现这些能力,而近几年来快速发展的深度神经网络因其强大的表征(representation)能力,提供了更多的可能性,在图像分类、机器翻译等领域不断推进机器智能的极限。借助深度神经网络,生成式自动文本摘要也有了令人瞩目的发展,不少生成式神经网络模型(neural-network-based abstractive summarization model)在DUC-2004测试集上已经超越了最好的抽取式模型[4]。这部分文章主要介绍生成式神经网络模型的基本结构及最新成果。基本模型结构生成式神经网络模型的基本结构主要由编码器(encoder)和解码器(decoder)组成,编码和解码都由神经网络实现。编码器负责将输入的原文本编码成一个向量(context),该向量是原文本的一个表征,包含了文本背景。而解码器负责从这个向量提取重要信息、加工剪辑,生成文本摘要。这套架构被称作Sequence-to-Sequence(以下简称Seq2Seq),被广泛应用于存在输入序列和输出序列的场景,比如机器翻译(一种语言序列到另一种语言序列)、image captioning(图片像素序列到语言序列)、对话机器人(如问题到回答)等。Seq2Seq架构中的编码器和解码器通常由递归神经网络(RNN)或卷积神经网络(CNN)实现。基于递归神经网络的模型RNN被称为递归神经网络,是因为它的输出不仅依赖于输入,还依赖上一时刻输出。如上图所示,t时刻的输出h不仅依赖t时刻的输入x,还依赖t-1时刻的输出,而t-1的输出又依赖t-1的输入和t-2输出,如此递归,时序上的依赖使RNN在理论上能在某时刻输出时,考虑到所有过去时刻的输入信息,特别适合时序数据,如文本、语音、金融数据等。因此,基于RNN实现Seq2Seq架构处理文本任务是一个自然的想法。典型的基于RNN的Seq2Seq架构如下图所示:图中展示的是一个用于自动回复邮件的模型,它的编码器和解码器分别由四层RNN的变种LSTM[5]组成。图中的向量thought vector编码了输入文本信息(Are you free tomorrow?),解码器获得这个向量依次解码生成目标文本(Yes, what’s up?)。上述模型也可以自然地用于自动文本摘要任务,这时的输入为原文本(如新闻),输出为摘要(如新闻标题)。目前最好的基于RNN的Seq2Seq生成式文本摘要模型之一来自Salesforce,在基本的模型架构上,使用了注意力机制(attention mechanism)和强化学习(reinforcement learning)。这个模型将在下文中详细介绍。基于卷积神经网络的模型 Seq2Seq同样也可以通过CNN实现。不同于递归神经网络可以直观地应用到时序数据,CNN最初只被用于图像任务[6]。CNN通过卷积核(上图的A和B)从图像中提取特征(features),间隔地对特征作用max pooling,得到不同阶层的、由简单到复杂的特征,如线、面、复杂图形模式等,如下图所示。CNN的优势是能提取出hierarchical的特征,并且能并行高效地进行卷积操作,那么是否能将CNN应用到文本任务中呢?原生的字符串文本并不能提供这种可能性,然而,一旦将文本表现成分布式向量(distributed representation/word embedding)[7],我们就可以用一个实数矩阵/向量表示一句话/一个词。这样的分布式向量使我们能够在文本任务中应用CNN。如上图所示,原文本(wait for the video and do n’t rent it)由一个实数矩阵表示,这个矩阵可以类比成一张图像的像素矩阵,CNN可以像“阅读”图像一样“阅读”文本,学习并提取特征。虽然CNN提取的文本特征并不像图像特征有显然的可解释性并能够被可视化,CNN抽取的文本特征可以类比自然语言处理中的分析树(syntactic parsing tree),代表一句话的语法层级结构。基于卷积神经网络的自动文本摘要模型中最具代表性的是由Facebook提出的ConvS2S模型[9],它的编码器和解码器都由CNN实现,同时也加入了注意力机制,下文将详细介绍。当然,我们不仅可以用同一种神经网络实现编码器和解码器,也可以用不同的网络,如编码器基于CNN,解码器基于RNN。前沿A Deep Reinforced Model for Abstractive Summarization这是由Salesforce研究发表的基于RNN的生成式自动文本摘要模型,通过架构创新和若干tricks提升模型概括长文本的能力,在CNN/Daily Mail、New York Times数据集上达到了新的state-of-the-art(最佳性能)。针对长文本生成摘要在文本摘要领域是一项比较困难的任务,即使是过去最好的深度神经网络模型,在处理这项任务时,也会出现生成不通顺、重复词句等问题。为了解决上述问题,模型作者提出了内注意力机制(intra-attention mechanism)和新的训练方法,有效地提升了文本摘要的生成质量。模型里应用了两套注意力机制,分别是1)经典的解码器-编码器注意力机制,和2)解码器内部的注意力机制。前者使解码器在生成结果时,能动态地、按需求地获得输入端的信息,后者则使模型能关注到已生成的词,帮助解决生成长句子时容易重复同一词句的问题。模型的另一创新,是提出了混合式学习目标,融合了监督式学习(teacher forcing)和强化学习(reinforcement learning)。首先,该学习目标包含了传统的最大似然。最大似然(MLE)在语言建模等任务中是一个经典的训练目标,旨在最大化句子中单词的联合概率分布,从而使模型学习到语言的概率分布。但对于文本摘要,仅仅考虑最大似然并不够。主要有两个原因,一是监督式训练有参考“答案”,但投入应用、生成摘要时却没有。比如t时刻生成的词是"tech",而参考摘要中是"science",那么在监督式训练中生成t+1时刻的词时,输入是"science",因此错误并没有积累。但在实际应用中,由于没有ground truth,t+1时刻的输入是错误的"tech"。这样引起的后果是因为没有纠正,错误会积累,这个问题被称为exposure bias。另一个原因是,往往在监督式训练中,对一篇文本一般只提供一个参考摘要,基于MLE的监督式训练只鼓励模型生成一模一样的摘要,然而正如在介绍中提到的,对于一篇文本,往往可以有不同的摘要,因此监督式学习的要求太过绝对。与此相反,用于评价生成摘要的ROUGE指标却能考虑到这一灵活性,通过比较参考摘要和生成的摘要,给出摘要的评价(见下文评估摘要部分)。所以希望在训练时引入ROUGE指标。但由于ROUGE并不可导的,传统的求梯度+backpropagation并不能直接应用到ROUGE。因此,一个很自然的想法是,利用强化学习将ROUGE指标加入训练目标。那么我们是怎么通过强化学习使模型针对ROUGE进行优化呢?简单说来,模型先以前向模式(inference)生成摘要样本,用ROUGE指标测评打分,得到了对这个样本的评价/回报(reward)后,再根据回报更新模型参数:如果模型生成的样本reward较高,那么鼓励模型;如果生成的样本评价较低,那么抑制模型输出此类样本。最终的训练目标是最大似然和基于ROUGE指标的函数的加权平均,这两个子目标各司其职:最大似然承担了建立好的语言模型的责任,使模型生成语法正确、文字流畅的文本;而ROUGE指标则降低exposure bias,允许摘要拥有更多的灵活性,同时针对ROUGE的优化也直接提升了模型的ROUGE评分。构建一个好的模型,除了在架构上需要有创新,也需要一些小技巧,这个模型也不例外。在论文中,作者使用了下列技巧:使用指针处理未知词(OOV)问题;共享解码器权重,加快训练时模型的收敛;人工规则,规定不能重复出现连续的三个词。综上所述,深度学习+强化学习是一个很好的思路,这个模型第一次将强化学习应用到文本摘要任务中,取得了不错的表现。相信同样的思路还可以用在其他任务中。Convolutional Sequence to Sequence LearningConvS2S模型由Facebook的AI实验室提出,它的编码器和解码器都是基于卷积神经网络搭建的。这个模型主要用于机器翻译任务,在论文发表的时候,在英-德、英-法两个翻译任务上都达到了state-of-the-art。同时,作者也尝试将该模型用于自动文本摘要,实验结果显示,基于CNN的Seq2Seq模型也能在文本摘要任务中达到接近state-of-the-art的表现。模型架构如下图所示。乍看之下,模型很复杂,但实际上,它的每个部分都比较直观,下面通过分解成子模块,详细介绍ConvS2S。首先来看embedding部分。这个模型的embedding比较新颖,除了传统的semantic embedding/word embedding,还加入了position embedding,将词序表示成分布式向量,使模型获得词序和位置信息,模拟RNN对词序的感知。最后的embedding是语义和词序embedding的简单求和。之后,词语的embedding作为输入进入到模型的卷积模块。这个卷积模块可以视作是经典的卷积加上非线性变换。虽然图中只画出一层的情况,实际上可以像经典的卷积层一样,层层叠加。这里着重介绍非线性变换。该非线性变换被称为Gated Linear Unit (GLU)[10]。它将卷积后的结果分成两部分,对其中一部分作用sigmoid变换,即映射到0到1的区间之后,和另一部分向量进行element-wise乘积。这个设计让人联想到LSTM中的门结构。GLU从某种程度上说,是在模仿LSTM和GRU中的门结构,使网络有能力控制信息流的传递,GLU在language modeling被证明是非常有效的[10]。除了将门架构和卷积层结合,作者还使用了残差连接(residual connection)[11]。residual connection能帮助构建更深的网络,缓解梯度消失/爆炸等问题。除了使用加强版的卷积网络,模型还引入了带多跳结构的注意力机制(multi-step attention)。不同于以往的注意力机制,多跳式注意力不仅要求解码器的最后一层卷积块关注输入和输出信息,而且还要求每一层卷积块都执行同样的注意力机制。如此复杂的注意力机制使模型能获得更多的历史信息,如哪些输入已经被关注过。像A Deep Reinforced Model for Abstractive Summarization一样,ConvS2S的成功之处不仅在于创新的结构,还在于细致入微的小技巧。在ConvS2S中,作者对参数使用了非常仔细的初始化和规范化(normalization),稳定了方差和训练过程。这个模型的成功证明了CNN同样能应用到文本任务中,通过层级表征长程依赖(long-range dependency)。同时,由于CNN具有可高度并行化的特点,所以CNN的训练比RNN更高效。比起RNN,CNN的不足是有更多的参数需要调节。评估摘要评估一篇摘要的质量是一件比较困难的任务。对于一篇摘要而言,很难说有标准答案。不同于很多拥有客观评判标准的任务,摘要的评判一定程度上依赖主观判断。即使在摘要任务中,有关于语法正确性、语言流畅性、关键信息完整度等标准,摘要的评价还是如同”一千个人眼里有一千个哈姆雷特“一样,每个人对摘要的优劣都有自己的准绳。自上世纪九十年代末开始,一些会议或组织开始致力于制定摘要评价的标准,他们也会参与评价一些自动文本摘要。比较著名的会议或组织包括SUMMAC,DUC(Document Understanding Conference),TAC(Text Analysis Conference)等。其中DUC的摘要任务被广泛研究,大多数abstractive摘要模型在DUC-2004数据集上进行测试。目前,评估自动文本摘要质量主要有两种方法:人工评价方法和自动评价方法。这两类评价方法都需要完成以下三点:决定原始文本最重要的、需要保留的部分;在自动文本摘要中识别出1中的部分;基于语法和连贯性(coherence)评价摘要的可读性(readability)。人工评价方法评估一篇摘要的好坏,最简单的方法就是邀请若干专家根据标准进行人工评定。这种方法比较接近人的阅读感受,但是耗时耗力,无法用于对大规模自动文本摘要数据的评价,和自动文本摘要的应用场景并不符合。因此,文本摘要研究团队积极地研究自动评价方法。自动评价方法为了更高效地评估自动文本摘要,可以选定一个或若干指标(metrics),基于这些指标比较生成的摘要和参考摘要(人工撰写,被认为是正确的摘要)进行自动评价。目前最常用、也最受到认可的指标是ROUGE(Recall-Oriented Understudy for Gisting Evaluation)。ROUGE是Lin提出的一个指标集合,包括一些衍生的指标,最常用的有ROUGE-n,ROUGE-L,ROUGE-SU:ROUGE-n:该指标旨在通过比较生成的摘要和参考摘要的n-grams(连续的n个词)评价摘要的质量。常用的有ROUGE-1,ROUGE-2,ROUGE-3。ROUGE-L:不同于ROUGE-n,该指标基于最长公共子序列(LCS)评价摘要。如果生成的摘要和参考摘要的LCS越长,那么认为生成的摘要质量越高。该指标的不足之处在于,它要求n-grams一定是连续的。ROUGE-SU:该指标综合考虑uni-grams(n = 1)和bi-grams(n = 2),允许bi-grams的第一个字和第二个字之间插入其他词,因此比ROUGE-L更灵活。 作为自动评价指标,ROUGE和人工评定的相关度较高,在自动评价摘要中能给出有效的参考。但另一方面,从以上对ROUGE指标的描述可以看出,ROUGE基于字的对应而非语义的对应,生成的摘要在字词上与参考摘要越接近,那么它的ROUGE值将越高。但是,如果字词有区别,即使语义上类似,得到的ROUGE值就会变低。换句话说,如果一篇生成的摘要恰好是在参考摘要的基础上进行同义词替换,改写成字词完全不同的摘要,虽然这仍是一篇质量较高的摘要,但ROUGE值会呈现相反的结论。从这个极端但可能发生的例子可以看出,自动评价方法所需的指标仍然存在一些不足。目前,为了避免上述情况的发生,在evaluation时,通常会使用几篇摘要作为参考和基准,这有效地增加了ROUGE的可信度,也考虑到了摘要的不唯一性。对自动评价摘要方法的研究和探索也是目前自动文本摘要领域一个热门的研究方向。总结本文主要介绍了基于深度神经网络的生成式文本摘要,包括基本模型和最新进展,同时也介绍了如何评价自动生成的摘要。自动文本摘要是目前NLP的热门研究方向之一,从研究落地到实际业务,还有一段路要走,未来可能的发展方向有:1)模仿人撰写摘要的模式,融合抽取式和生成式模型;2)研究更好的摘要评估指标。希望本文能帮助大家更好地了解深度神经网络在自动文本摘要任务中的应用。Reference[1] Text Summarization Techniques: A Brief Survey[2] A Survey on Automatic Text Summarization[3] Introduction to the Special Issue on Summarization[4] A Deep Reinforced Model for Abstractive Summarization[5] Understanding LSTM Networks[6] LeNet5, convolutional neural networks[7] What is word embedding in deep learning[8] A Deep Reinforced Model for Abstractive Summarization[9] Convolutional Sequence to Sequence Learning[10] Language Modeling with Gated Convolutional Networks[11]Deep Residual Learning for Image Recognition问答深度学习的原理和具体实现是什么?相关阅读深度学习角度 | 图像识别将何去何从?基于深度学习的文本分类?10分钟上手,OpenCV自然场景文本检测(Python代码+实现) 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 30, 2018 · 1 min · jiezi

打造 Laravel 优美架构 谈可维护性与弹性设计

公司项目可能需要对架构进行重建,老大给了我一个视频让我学习里面的思想,看完后觉得收获很大,主讲人对laravel项目各个层次有很清晰的理解,力求做到职责单一分明,提高可维护性。下面是我看完视频对其内容的大概整理,以及一些自己的见解,有错误的请指出。视频:https://www.youtube.com/watch… (有墙各位懂的)Laravel简单架构:简单的小项目可能会把数据库查询,业务逻辑,数据传给View几乎所有操作都放在Controller,如何项目后期需求变大,最后Controller会变得很臃肿,难懂,不易维护(同样,有些会把所有增删改查,功能类写在Model,Controller再从Model一个个的拿,导致Model很乱,Model有关联表的时候可能会引起一些不必要的数据库查询)我自己的理解:用美宜佳卖商品给客人来理解,主要Controller是某个加盟商美宜佳门店,View是客人,Model是商品制造工厂(理解有些粗糙)Repository(商品仓库):跟Eloquent/DB操作相关的,例如增删改查,直接和数据库打交道的基础操作抽出来放在Repository中,repository中文是仓库,我的理解就是我们要从Model拿数据,先放在仓库repository中,统一由仓库管理分配,发挥仓库的职责Service(总部服务平台):商业逻辑,不是简单的查询数据,而是特定的任务,例如判断用户是否是会员,设置用户权限等等,这些操作建议放在Service,之后Controller再调用它个人理解:所以在Controller和Model/Eloquent中间垫两层,如果Repository理解为商品仓库的话,我的理解Service是类似总部内部的服务平台,加盟商Controller需要拿商品给客人View,不能直接去食品工厂Model拿,先通过仓库repository,然后总部服务平台Service进行打包啊,整理啊,发车啊(各种任务),最后再给到加盟商Controller手里Presenter(充值业务):一些比较固定,可以单独调用的,可以用Presenter抽出来,不需要让Model去做,下次修改也单独修改Presenter就行了,例如时间戳转成Y-m-d H:i:s格式,可以单独用Presenter处理后用@inject插入到前端模板,而不是把转化过程写在模板上面个人理解:所以在Controller和View中间可以加一层Presenter,我的理解有点类似:美宜佳商户(Controller)可以给客人(View)充公交卡,这种小事不需要劳费工厂(Model)Transformer(快餐小吃人工筛选):转换器,例如在仓库repository中有一个获取所有用户信息的查询操作:$this->user->all();但有些地方我们不需要用到那么多个字段,我只想有name和email字段,难道我要去改all()里面的参数,变成$this->user->all([’name’,’email’])?这样另外的地方又要全部字段,这不就冲突了?这时候Transformer就有用了,其实原理是对$this->user->all()获得的数据进行筛选后再输出,加了个筛选器。之后要修改结果字段就直接在transform修改即可,当然还可以额外添加需要的字段:array_set()个人理解:这一块我的理解就是有些客人需要点一些快餐,例如美宜佳里面的车仔面呀,烤肠呀,在卖出商品的时候需要根据客人的需求对小吃进行筛选再卖出去,不可能客人指点要一个烤肠,你把店里全部小吃拿给他,让他自个去筛选,中间卖出去的时候需要Transformer进行筛选再给出商品Formatter(包装):主要用于保持API返回格式的一致(使用方法和transform类似):个人理解:Formatter这一块我的理解就是商品包装,客人买东西,买小吃,你需要对商品先进行包装,当然这个包装肯定需要保持一致以上便是我再看完视频后对其进行总结整理,当然理论的说的容易,实际操作起来还有很多未知的问题,还是需要后面继续研究学习。

September 7, 2018 · 1 min · jiezi

练就Java24章真经—你所不知道的工厂方法

前言最近一直在Java方向奋斗《终于,我还是下决心学Java后台了》,今天抽空开始学习Java的设计模式了。计划有时间就去学习,你这么有时间,还不来一起上车吗?之所以要学习Java模式,是因为面试的时候有时间回答的不是太完整,面试过后才想起来如何回答。所以,我说了: 只有总结才是王道,只有总结才能提高设计模式其实正规的来说Java其实是23中设计模式,不过网上也有说是24种或者是26中的!设计模式不过是前人对代码的一种封装。用专业的话来讲:设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结创建型模式,共五种:1.工厂方法模式、2.抽象工厂模式、3.单例模式、4.建造者模式、5.原型模式。结构型模式,共七种:6.适配器模式、7.装饰器模式、8.代理模式、9.外观模式、10.桥接模式、11.组合模式、12.享元模式。行为型模式,共十一种:13.策略模式、14.模板方法模式、15.观察者模式、16.迭代子模式、17.责任链模式、18.命令模式、19.备忘录模式、20.状态模式、21.访问者模式、22.中介者模式、23.解释器模式。今日重点:工厂方法模式工厂模式是创建型模式之一,又称为静态工厂方法模式!优点:1.良好的封装性,代码结构清晰。一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,减少模块间的耦合。2.工厂方法模式的扩展性非常优秀。在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化”。例如在我们的例子中,需要增加一个棕色人种,则只需要增加一个BrownHuman类,工厂类不用任何修改就可完成系统扩展。3.屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不表,系统中的上层模块就不要发生变化,因为产品类的实例化工作是由工厂类负责,一个产品对象具体由哪一个产品生成是由工厂类决定的。在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从MySql切换到Oracle,需要改动地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活性的一个直接案例。4.工厂方法模式是典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特原则,我不需要的就不要去交流;也符合依赖倒转原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类,没问题!缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,是的系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。用途:第一种情况是对于某个产品,调用者清楚地知道应该使用哪个具体工厂服务,实例化该具体工厂,生产出具体的产品来。Java Collection中的iterator() 方法即属于这种情况。第二种情况,只是需要一种产品,而不想知道也不需要知道究竟是哪个工厂为生产的,即最终选用哪个具体工厂的决定权在生产者一方,它们根据当前系统的情况来实例化一个具体的工厂返回给使用者,而这个决策过程这对于使用者来说是透明的。典型例子:车子继承vehicle(车)类,有小汽车卡,公交车bus等,车子工厂实现工厂接口,工厂接口有抽象方法vehicle produce vehicle(String type)方法,车子工厂中实现工厂方法vehicle produce vehicle(String Type),方法中根据需要new新的车子。示例代码:注意事项有人把工厂模式分为: 简单工厂模式 ,工厂方法模式,抽象工厂模式,所以多出一种模式,这里简单工厂模式比较简单,实际中用的的很少,只在很简单的情况下用,没啥好说的,据说这不是一个真正的设计模式。在这里我就不做讨论了。希望 大家也不用纠结!项目地址:https://github.com/androidsta…总结学习一个知识点要知道是什么,为什么,怎么办,要知其然。也要知其所以然!阅读更多终于,我还是下决心学Java后台了来谈一下android中的MVVM金9银10的面试黄金季节,分享几个重要的面试题身为程序员写一百万行代码的感觉相信自己,没有做不到的,只有想不到的在这里获得的不仅仅是技术!

September 5, 2018 · 1 min · jiezi