李智慧 高并发架构实战课(四)
19 | 许可型区块链重构:无中心的区块链怎么做到可信任?
一般我们把对所有公众都开放访问的区块链叫做“公有链”,而把若干企业构建的仅供企 业间访问的区块链叫做“联盟链”,有时候也称作“许可型区块链”。
而在公有链领域,目前看来,生态最完整、开发者社区最活跃、去中心化应用最多的公有 链技术莫过于 Ethereum 以太坊。
以我们准备在以太坊的代码基础上,进行若干代码模块的重构与开发。开发一个基于以太坊的企业级分布式账本与智能合 约平台,即一个许可型区块链。这个许可型区块链产品名称为“Taireum”。
需求分析
所谓区块链(block chain),就是将不断产生的数据按时间序列分组成一个一个连续的数 据区块(block),然后利用单向散列加密算法,求取每个区块的 Hash 值,并且在每个区 块中记录前一个区块的 Hash 值,这些区块就通过 Hash 值串成一个链条,被形象地称为 区块链。如
相比于比特币,以太坊最大的技术特点是支持智能合约,它是一种存储在区块链上的程 序,由链上的计算机节点分布式运行,是一种去中心化的应用程序,也是区块链企业级应 用必需的技术要求。
是以太坊是一种公有链技术,并不适合用于企业级的场景,原因主要有三个
- 在准入机制上,使用以太坊构建的区块链网络允许任何节点接入
- 在共识算法上,以太坊使用工作量证明(PoW)的方式对区块打包进行算力证明.工作量证明需要花费巨大的计算资源进行算力证 明,造成算力的极大浪费,也影响了区块链的交易吞吐能力。
- 在区块链运维管理上,以太坊作为公有链,节点之间通过 P2P 协议自动组网,无需运维管理。
Taireum 需要在以太坊的基础上进行如下重构:
- 重构以太坊的 P2P 网络通信模块,使其需要进行安全验证,得到联盟许可才能加入新节点,进入当前联盟链网络。
- 重构以太坊的共识算法。只有经过联盟成员认证授权的节点才能打包区块,打包节点按序轮流打包,无需算力证明。
- 开发联盟共识控制台 CCC(Consortium Consensus Console),方便对联盟链进行运维管理,联盟链用户只需要在 web console 上就可以安装部署联盟链节点,投票选举新 的联盟成员和区块授权打包节点。
概要分析
使用 Taireum 部署的联盟链如图:
Taireum 部署模型如下:
- Taireum 中每个联盟企业都是一个 Taireum 节点,都需要完整地部署 Taireum+CCC 控制台, Client 使用我们提供的 web3jPlus sdk 与 Geth 进行 RPC 通信。
- Geth 是 Tairem 编译出来的区块链运行程序,里面包含重写的 Tai 共识算法,重构后的 P2P 网络模块,以及原始的以太坊代码。
- 不同节点之间的 Geth 使用 P2P 网络进行通信。
详细分析
Taireum 联盟共识控制台
联盟链运维管理开发的 web 组件,企业可以非常方便地使 用联盟共识控制台来部署联盟链运行节点、管理联盟成员和授权节点打包区块。
每个企业 节点的联盟共识控制台彼此独立,互不感知。他们需要通过调用联盟共识智能合约,对联 盟管理事务进行协商,以达成共识。
联盟链创立者节点的联盟共识控制台第一次成功部署联盟共识智能合 约时,就把这个合约的地址发给共识算法模块。共识算法在封装区块头的时候,将合约地 址写入区块头的 miner 中。
Taireum 联盟新成员许可入网
联盟链需要保证联盟内数据的隐私和安全。
Taireum 重构了以太坊的 P2P 通信模块,只有在许可列表中的节点才允许和当前联盟成员 节点建立连接,其他的连接请求在通信模块就会被拒绝,以此保证联盟链的安全和私密性。
Taireum 联盟新成员许可入网流程:
- 新成员下载 Taireum,启动联盟共识控制台,然后在联盟共识控制台启动 Taireum 节点,获得节点 enode url。
- 将 enode url 及公司信息提交给当前联盟链某个成员,该成员通过联盟共识智能合约发起新成员入网申请。
- 联盟其他成员通过智能合约对新成员入网申请进行投票,得票数符合约定后,新成员信 息被记入成员列表。
- 新成员节点通过网络连接当前联盟链成员节点,当前成员节点 p2p 通信模块读取智能合 约成员列表信息,检查新成员节点 enode url 是否在成员列表中,如果在,就同意建立连接,新成员节点开始下载区块数据。
Taireum 授权打包区块
Taireum 根据联盟链的应用特点,放弃了以太坊 ethash 工作量证明算法。在借鉴 clique 共识算法的基础上,Taireum 重新开发了 Tai 共识算法引擎,对联盟投票选出的授权打包节点排序,轮流进行区块打包。
Tai 共识算法引擎执行过程如下:
- 联盟成员通过联盟共识智能合约投票选举授权打包区块的节点(在合约创建的时候,创 建者即联盟链创始人默认拥有打包区块的权限)。
- Tai 共识算法通过联盟共识控制台访问智能合约,获得授权打包区块的节点地址列表, 并排序。
- 检查父区块头的 extraData,解密取出父区块的打包者签名,查看该签名是否在授权打 包节点地址列表里,如果不在就返回错误。
- 根据当前区块的块高(block number),对授权打包区块的节点地址列表长度取模,根 据余数决定对当前区块进行打包的节点,如果计算出来的打包节点为当前节点,就进行 区块打包,并把区块头难度系数设为 2,如果非当前节点,随机等待一段时间后打包区 块,并把区块头难度系数设为 1。难度系数的目的是尽量使当前节点打包的区块被加入 区块链,同时又保证当前打包节点失效的情况下,其他节点也会完成区块打包的工作。
Taireum 源码:https://github.com/taireum/go-taireum
20 | 网约车系统设计:怎样设计一个日赚 5 亿的网约车系统?
中国目前网约车用户规模约 5 亿,我们准备开发一个可支撑目前全部中国用户使用车平台,应用名称为“Udi”。
需求分析
用例图如下
Udi 平台预计注册乘客 5 亿,日活用户 5 千万,平均每个乘客 1.2 个订单,日订单量 6 千 万。平均客单价 30 元,平台每日总营收 18 亿元。平台和司机按 3:7 的比例进行分成, 那么平台每天可赚 5.4 亿元。
另外,平台预计注册司机 5 千万,日活司机 2 千万。
概要设计
需要开发两个 App 应用,一个是给乘客的,用来叫车;一个是给司机的,用来接单。Udi 整体架构如下图:
详细设计
- 关注网约车平台一些独有的技术特点:长连接管理、派单算 法、距离计算。
- 讨论 Udi 的订单状态模型。(所有交易类应用都非常重要的一个模型)
长连接管理
我们选择让司机 App 和 Udi 系统 直接通过 TCP 协议进行长连接。一旦建立了连接,连接通道就需要长期保持,不管是司机 App 发送位 置信息给服务器,还是服务器推送派单信息给司机App,都需要使用这个特定的连接通道。
司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,具体架构如下图。
处理长连接的核心是 TCP 管理服务器集群。司机 App 会在启动时通过负载均衡服务器, 与TCP 管理服务器集群通信,请求分配一个 TCP 长连接服务器。
长连接管理的主要时序图如下
如果TCP服务器宕机,长连接丢失。司机 App 需要重新通过 HTTP 来请求 TCP 管理服务器为它分配新的 TCP 服务器。TCP 管理服务器收到请求后, 一方面返回新的 TCP 服务器的 IP 地址和通信端口,一方面需要从 Redis 中删除原有的<司机 ID, 服务器名>键值对。
距离计算
Udi 就是直接使用 Redis 的 GeoHash 进行邻近计算。司机的位置信息实时更新到 Redis 中,并直接调用 Redis 的 GeoHash 命令 georadius 计算乘客的邻近司机。
但是 Redis 使用跳表存储 GeoHash,Udi 日活司机两千万,每 3 秒更新一次位置信息, 平均每秒就需要对跳表做将近 7 百万次的更新,如此高并发地在一个跳表上更新,是系统不能承受的。所以,我们需要将司机以及跳表的粒度拆得更小。
Udi 以城市作为地理位置的基本单位,也就是说,每个城市在 Redis 中建立一个 GeoHash 的 key,这样,一个城市范围内的司机存储在一个跳表中。对于北京这样的超级城市,还 可以更进一步,以城区作为 key,进一步降低跳表的大小和单个跳表上的并发量。
派单算法
Redis 计算的是两个点之间的空间距离,但是司机在城市的路线未必是直线。
派单算法需要从 Redis 中获取多个邻近用户上车点的空闲司机,然 后通过地理系统来计算每个司机到达乘客上车点的时间,最后将订单分配给花费时间最少的司机。
我们需要将一批订单聚合在一起,统一进行派单,如下图:
分单子系统收到用户的叫车订单后,不是直接发送给派单引擎进行派单,而是发给一个订
单聚合池,订单聚合池里有一些订单聚合桶。订单写完一个聚合桶,就把这个聚合桶内的全部订单推送给派单引擎,由派单引擎根据整体时间最小化原则进行派单。
这里更倾向于是司机的总时间最小,因为司机的时间就是平台的金钱
这里的“写完一个聚合桶”,有两种实现方式,一种是间隔一段时间算写完一个桶,一种 是达到一定数量算写完一个桶。最后 Udi 选择间隔 3 秒写一个桶。
派单的时候需要依赖地理系统进行路径规划。事实上,乘客到达时间 和金额预估、行驶过程导航、订单结算与投诉处理,都需要依赖地理系统。Udi 初期会使 用第三方地理系统进行路径规划,但是将来必须要建设自己的地理系统。
订单状态模型
散乱的订单状态变化无法统一描述订单的完整生命周期,因此我们设计了订单状态模型,如下图
订单状态模型可以帮助我们总览核心业务流程,在设计阶段,可以通过状态图发现业务流程不完备的地方,在开发阶段,可以帮助开发者确认流程实现是否有遗漏。
21 | 网约车系统重构:如何用 DDD 重构网约车系统设计?
DDD 的一般方法
领域驱动设计就是从领域出发,分析领域内模型及其关系, 进而设计软件系统的方法。
如果我们说要对 C2C 电子商务这个领域进行建模设计,那么这个范围就太大了,不知 道该如何下手。所以通常的做法是把整个领域拆分成多个子域,比如用户、商品、订单、 库存、物流、发票等。强相关的多个子域组成一个限界上下文。
不同的限界上下文,也就是不同的子系统或者模块之间会有各种的交互合作——>DDD 使用上下文映射图来完成。
在 DDD 中,领域模型对象也被称为实体。先通过业务分析,识别出实体对象,然后通过相关的业务逻辑,设计实体的属性和方法。而限界上下文和上下文映射图则是微服务设计的关键,通常在实践中,限界上下文被设计为微服务,而上下文映射图就是微服务之间的依赖关系。具体设计过程如下图:
首先,领域专家和团队一起讨论分析业务领域,确认业务期望,将业务分解成若干个业务场景。然后,针对每个场景画 UML 活动图,活动图中包含泳道,通过高内聚原则对功能 逻辑不断调整,使功能和泳道之间的归属关系变得更加清晰合理。这些泳道最终就是限界上下文,泳道内的功能就是将来微服务的功能边界,泳道之间的调用流程关系,就是将来 微服务之间的依赖关系,即上下文映射图。
根据康威定律:组织架构决定系统架构,两个团队维护一个微服务,必然会将这个微服务搞成事 实上的两个微服务。所以,我们还需要根据团队特性、过往的工作职责、技能经验,重新 对泳道图进行调整,使其符合团队的职责划分,这时候才得到限界上下文。
在这个限界上下文基础上,考虑技术框架、非功能需求、服务重用性等因素,进一步进行调整,就得到最终的限界上下文设计,形成我们的微服务架构设计。
Udi DDD 重构设计
首先分析我们的业务领域,通过头脑 / 事件风暴的形式,收集领域内的所有事件 / 命令, 并识别事件 / 命令的发起方即对应的实体。最后识别出来的实体以及相关活动如下表:
基于核心实体模型,绘制实体关系图,如下:
在实体间关系明确且完整的前提下,我们就可以针对各个业务场景,绘制场景活动图。活动图比较多,这里仅用拼车场景作为示例
依据各种重要场景的活动图,参考团队职责范围,结合微服务重用性考虑及非功能需求,产生限界上下文如下表:
针对每个限界上下文进一步设计其内部的聚合、聚合根、实体、值对象、功能边界。以订单限界上下文为例:
上述订单实体的属性和功能如下表:
最后,在实现层面,设计对应的微服务架构如下图:
这是一个基于领域模型的分层架构,最下层为聚合根对象,组合实体与值对象,完成核心业务逻辑处理。上面一层为领域服务层,主要调用聚合根对象完成订单子域的业务,根据业务情况,也会在这一层和其他微服务通信,完成更复杂的、超出当前实体职责的业务,所以这一层也是一个聚合层。
再上面一层是应用服务层,将实体的功能封装成各种服务,供各种应用场景调用。而最上面是一个接口层,提供微服务调用接口。
小结
我们把使用 DDD 进行系统重构的过程分为以下六步:
- 讨论当前系统存在的问题,发现问题背后的根源。比如:架构与代码混乱,需求迭代困 难,部署麻烦,bug 率逐渐升高;微服务边界不清晰,调用依赖关系复杂,团队职责混 乱。
- 针对问题分析具体原因。比如:微服务 A 太庞大,微服务 B 和 C 职责不清,团队内业 务理解不一致,内部代码设计不良,硬编码和耦合太多。
- 重新梳理业务流程,明确业务术语,进行 DDD 战略设计,具体又可以分为三步。
a. 进行头脑风暴,分析业务现状和期望,构建领域语言;
b. 画泳道活动图、结合团队特性设计限界上下文;
c. 根据架构方案和非功能需求确定微服务设计。 - 针对当前系统实现和 DDD 设计不匹配的地方,设计微服务重构方案。比如:哪些微服 务需要重新开发,哪些微服务的功能需要从 A 调整到 B,哪些微服务需要分拆。
- DDD 技术验证。针对比较重要、问题比较多的微服务进行重构打样,设计聚合根、实 体、值对象,重构关键代码,验证设计是否合理以及团队能否驾驭 DDD。
- 任务分解与持续重构。在尽量不影响业务迭代的前提下,按照重构方案,将重构开发和 业务迭代有机融合。
22 | 大数据平台设计:如何用数据为用户创造价值?
大数据技术
则要将这些海量的用户数据进行关联计算,因此,适用于高并发架构的各种分布式技术并不能解决大数据的问题。
Udi 大数据平台设计
根据 Udi 大数据应用场景的需求,需要将手机 App 端数据、数据库订单和用户数据、操作 日志数据、网络爬虫爬取的竞争对手数据统一存储到大数据平台,并支持数据分析师、算法工程师提交各种 SQL 语句、机器学习算法进行大数据计算,并将计算结果存储或返回。 Udi 大数据平台架构如下图:(蓝色属于大数据平台的组件)
大数据采集与导入
Udi 大数据平台整体可分为三个部分,第一个部分是大数据采集与导入。这一部分又可以 分为 4 小个部分,App 端数据采集、系统日志导入、数据库导入、爬虫数据导入。
后端的应用上报服务器收到前端采集的数据后,发送给消息队列,SparkStreamin 从消息 队列中消费消息,对数据进行清洗、格式化等 ETL 处理,并将数据写入到 HDFS 存储中。
Flume 日志收集系统会将 Udi 后 端分布式集群中的日志收集起来,发送给 SparkStreaming 进行 ETL 处理,最后写入到 HDFS 中。而 MySQL 的数据则通过 Sqoop 数据同步系统直接导入到 HDFS 中。
为了更好地应对市场竞争,Udi 还会通过网络爬 虫从竞争对手的系统中爬取数据。(模拟为普通用户)
大数据计算
Udi 大数据平台的第二个部分是大数据计算。写入到 HDFS 中的数据,一方面供数据分析 师进行统计分析,一方面供算法工程师进行机器学习。
数据分析师会通过两种方式分析数据。
一种是通过交互命令进行即席查询,通常是一些较为简单的 SQL。分析师提交 SQL 后,在一个准实时、可接受的时间内返回查询结果,这个 功能是通过 Impala 完成的。
另外一种是定时 SQL 统计分析,通常是一些报表类统计,这 些 SQL 一般比较复杂,需要关联多张表进行查询,耗时较长,通过 Hive 完成,每天夜间服务器空闲的时候定时执行。
算法工程师则开发各种 Spark 程序,基于 HDFS 中的数据,进行各种机器学习。
以上这些大数据计算组件,Hive、Spark、SparkStreaming、Impala 都部署在同一个大 数据集群中,通过 Yarn 进行资源管理和调度执行。每台服务器既是 HDFS 的 DataNode 数据存储服务器,也是 Yarn 的 NodeManager 节点管理服务器,还是 Impala 的 Impalad 执行服务器。通过 Yarn 的调度执行,这些服务器上既可以执行 SparkStreaming 的 ETL 任务,也可以执行 Spark 机器学习任务,而执行 Hive 命令的时候,这些机器上运 行的是 MapReduce 任务。
数据导出与应用
需要用 Sqoop 将 HDFS 中的数据导出到 MySQL 中,然后通过数据分析查询控制台,以图表的方式查看数据。
机器学习的计算结果则是一些学习模型或者画像数据,将这些数据推送给推荐引擎,由 推荐引擎实时响应 Udi 系统的推荐请求。
Udi 大数据派单引擎设计
我们将利用这些数据优化 Udi 派单引擎。根据用户画像、车辆画像、乘车偏好进行同类匹配。
基于乘客分类的匹配
根据乘客的注册信息、App 端采集的乘客手机型号、手机内安装应用列表、常用上下车地 点等,我们可以将乘客分类,然后根据同类乘客的乘车偏好,预测乘客的偏好并进行匹配。
基于车辆分类的匹配
使用推荐引擎对派单系统进行优化,为乘客分配更合适的车辆,前提是需要对用户和车辆 进行分类与画像,想要完成这部分工作,我们可以在大数据平台的 Spark 机器学习模块通 过聚类分析、分类算法、协同过滤算法,以及 Hive 统计分析模块进行数据处理,将分类后 的数据推送给派单引擎去使用。