SQL Server数据库大型应用解决方案总结-技术开发专区

mikel阅读(826)

随着互联网应用的广泛普及,海量数据的存储和访问成为了系统设计的瓶颈问题。对于一个大型的互联网应用,每天百万级甚至上亿的PV无疑对数据库造成了相当高的负载。对于系统的稳定性和扩展性造成了极大的问题。

来源: SQL Server数据库大型应用解决方案总结-技术开发专区

【IT168 技术】随着互联网应用的广泛普及,海量数据的存储和访问成为了系统设计的瓶颈问题。对于一个大型的互联网应用,每天百万级甚至上亿的PV无疑对数据库造成了相当高的负载。对于系统的稳定性和扩展性造成了极大的问题。

一、负载均衡技术

负载均衡集群是由一组相互独立的计算机系统构成,通过常规网络或专用网络进行连接,由路由器衔接在一起,各节点相互协作、共同负载、均衡压力,对客户端来说,整个群集可以视为一台具有超高性能的独立服务器。

1、实现原理

实现数据库的负载均衡技术,首先要有一个可以控制连接数据库的控制端。在这里,它截断了数据库和程序的直接连接,由所有的程序来访问这个中间层,然后再由中间层来访问数据库。这样,我们就可以具体控制访问某个数据库了,然后还可以根据数据库的当前负载采取有效的均衡策略,来调整每次连接到哪个数据库。

2、实现多数据库数据同步

对于负载均衡,最重要的就是所有服务器的数据都是实时同步的。这是一个集群所必需的,因为,如果数不据实时、不同步,那么用户从一台服务器读出的数据,就有别于从另一台服务器读出的数据,这是不能允许的。所以必须实现数据库的数据同步。这样,在查询的时候就可以有多个资源,实现均衡。比较常用的方法是Moebius for SQL Server集群,Moebius for SQL Server集群采用将核心程序驻留在每个机器的数据库中的办法,这个核心程序称为Moebius for SQL Server 中间件,主要作用是监测数据库内数据的变化并将变化的数据同步到其他数据库中。数据同步完成后客户端才会得到响应,同步过程是并发完成的,所以同步到多个数据库和同步到一个数据库的时间基本相等;另外同步的过程是在事务的环境下完成的,保证了多份数据在任何时刻数据的一致性。正因为Moebius 中间件宿主在数据库中的创新,让中间件不但能知道数据的变化,而且知道引起数据变化的SQL语句,根据SQL语句的类型智能的采取不同的数据同步的策略以保证数据同步成本的最小化。

SQL Server数据库大型应用解决方案总结

数据条数很少,数据内容也不大,则直接同步数据

数据条数很少,但是里面包含大数据类型,比如文本,二进制数据等,则先对数据进行压缩然后再同步,从而减少网络带宽的占用和传输所用的时间。

数据条数很多,此时中间件会拿到造成数据变化的SQL语句, 然后对SQL语句进行解析,分析其执行计划和执行成本,并选择是同步数据还是同步SQL语句到其他的数据库中。此种情况应用在对表结构进行调整或者批量更改数据的时候非常有用。

3、优缺点

(1) 扩展性强:当系统要更高数据库处理速度时,只要简单地增加数据库服务器就 可以得到扩展。

(2) 可维护性:当某节点发生故障时,系统会自动检测故障并转移故障节点的应用,保证数据库的持续工作。

(3) 安全性:因为数据会同步的多台服务器上,可以实现数据集的冗余,通过多份数据来保证安全性。另外它成功地将数据库放到了内网之中,更好地保护了数据库的安全性。

(4) 易用性:对应用来说完全透明,集群暴露出来的就是一个IP

(1) 不能够按照Web服务器的处理能力分配负载。

(2) 负载均衡器(控制端)故障,会导致整个数据库系统瘫痪。

二、数据库的读写分离

1,实现原理:读写分离简单的说是把对数据库读和写的操作分开对应不同的数据库服务器,这样能有效地减轻数据库压力,也能减轻io压力。主数据库提供写操作,从数据库提供读操作,其实在很多系统中,主要是读的操作。当主数据库进行写操作时,数据要同步到从的数据库,这样才能有效保证数据库完整性。

SQL Server:数据库的读写分离
▲(ebay的读写比率是260:1,ebay的读写分离)

SQL Server:数据库的读写分离
▲(微软数据库分发)

2,实现方法:在MS Sql server中可以使用发布定义的方式实现数据库复制,实现读写分离,复制是将一组数据从一个数据源拷贝到多个数据源的技术,是将一份数据发布到多个存储站点上的有效方式。使用复制技术,用户可以将一份数据发布到多台服务器上。复制技术可以确保分布在不同地点的数据自动同步更新,从而保证数据的一致性。SQL SERVER复制技术类型有三种,分别是:快照复制、事务复制、合并复制。SQL SERVER 主要采用出版物、订阅的方式来处理复制。源数据所在的服务器是出版服务器,负责发表数据。出版服务器把要发表的数据的所有改变情况的拷贝复制到分发服务器,分发服务器包含有一个分发数据库,可接收数据的所有改变,并保存这些改变,再把这些改变分发给订阅服务器。

3,优缺点

(1)数据的实时性差:数据不是实时同步到自读服务器上的,当数据写入主服务器后,要在下次同步后才能查询到。

(2)数据量大时同步效率差:单表数据量过大时插入和更新因索引,磁盘IO等问题,性能会变的很差。

(3)同时连接多个(至少两个)数据库:至少要连接到两个数据数据库,实际的读写操作是在程序代码中完成的,容易引起混乱

(4)读具有高性能高可靠性和可伸缩:只读服务器,因为没有写操作,会大大减轻磁盘IO等性能问题,大大提高效率;只读服务器可以采用负载均衡,主数据库发布到多个只读服务器上实现读操作的可伸缩性。

三、数据库/数据表 拆分(分布式)

通过某种特定的条件,将存放在同一个数据库中的数据分散存放到多个数据库上,实现分布存储,通过路由规则路由访问特定的数据库,这样一来每次访问面对的就不是单台服务器了,而是N台服务器,这样就可以降低单台机器的负载压力。提示:SQLServer 2005版本之后,可以友好的支持“表分区”。

垂直(纵向)拆分:是指按功能模块拆分,比如分为订单库、商品库、用户库…这种方式多个数据库之间的表结构不同。

水平(横向)拆分:将同一个表的数据进行分块保存到不同的数据库中,这些数据库中的表结构完全相同。

SQL Server:数据库/数据表 拆分
▲(纵向拆分)

SQL Server:数据库/数据表 拆分
▲(横向拆分)

1,实现原理:使用垂直拆分,主要要看应用类型是否合适这种拆分方式,如系统可以分为,订单系统,商品管理系统,用户管理系统业务系统比较明的,垂直拆分能很好的起到分散数据库压力的作用。业务模块不明晰,耦合(表关联)度比较高的系统不适合使用这种拆分方式。但是垂直拆分方式并不能彻底解决所有压力问题,例如 有一个5000w的订单表,操作起来订单库的压力仍然很大,如我们需要在这个表中增加(insert)一条新的数据,insert完毕后,数据库会针对这张表重新建立索引,5000w行数据建立索引的系统开销还是不容忽视的,反过来,假如我们将这个表分成100个table呢,从table_001一直到table_100,5000w行数据平均下来,每个子表里边就只有50万行数据,这时候我们向一张只有50w行数据的table中insert数据后建立索引的时间就会呈数量级的下降,极大了提高了DB的运行时效率,提高了DB的并发量,这种拆分就是横向拆分

2,实现方法:垂直拆分,拆分方式实现起来比较简单,根据表名访问不同的数据库就可以了。横向拆分的规则很多,这里总结前人的几点,

(1)顺序拆分:如可以按订单的日前按年份才分,2003年的放在db1中,2004年的db2,以此类推。当然也可以按主键标准拆分。

优点:可部分迁移

缺点:数据分布不均,可能2003年的订单有100W,2008年的有500W。

(2)hash取模分: 对user_id进行hash(或者如果user_id是数值型的话直接使用user_id的值也可),然后用一个特定的数字,比如应用中需要将一个数据库切分成4个数据库的话,我们就用4这个数字对user_id的hash值进行取模运算,也就是user_id%4,这样的话每次运算就有四种可能:结果为1的时候对应DB1;结果为2的时候对应DB2;结果为3的时候对应DB3;结果为0的时候对应DB4,这样一来就非常均匀的将数据分配到4个DB中。

优点:数据分布均匀

缺点:数据迁移的时候麻烦;不能按照机器性能分摊数据 。

(3)在认证库中保存数据库配置

就是建立一个DB,这个DB单独保存user_id到DB的映射关系,每次访问数据库的时候都要先查询一次这个数据库,以得到具体的DB信息,然后才能进行我们需要的查询操作。

优点:灵活性强,一对一关系

缺点:每次查询之前都要多一次查询,会造成一定的性能损失。

SQL Server 2012中的AlwaysOn尝试 - CareySon - 博客园

mikel阅读(600)

来源: SQL Server 2012中的AlwaysOn尝试 – CareySon – 博客园

简介

SQL Server2012中新增的AlwaysOn是一个新增高可用性解决方案。在AlwaysOn之前,SQL Server已经有的高可用性和数据恢复方案,比如数据库镜像,日志传送和故障转移集群.都有其自身的局限性。而AlwaysOn作为微软新推出的解决方案,提取了数据库镜像和故障转移集群的优点。本文旨在通过实现一个AlwaysOn的实例来展现AlwaysOn。

 

配置AlwaysOn

虽然AlwaysOn相比较之前版本的故障转移集群而言,步骤已经简化了许多。但配置AlwaysOn依然是一件比较麻烦的事,不仅需要你对SQL Server比较熟悉,还需要对Windows Server有所了解。本文配置AlwaysOn分为两个板块,分别为:配置Windows和配置SQL Server。

在开始说道配置Windows之前,首先简单介绍一下测试环境。

我搭了三台Windows Server 2008的虚拟机(SQL SERVER 2012比较麻烦,必须2008 sp2以上版本windows server才能安装),如图1所示。其中将活动目录和DNS服务器安在了Windows Server2008 Server 1.没有启用DHCP服务器,三台服务器的IP分别为192.168.100.1/24,192.168.100.2/24,192.168.100.3/24。

1

图1.三台装有Windows Server2008的测试虚拟机

 

三台服务器都加入了由Windows Server 2008 Server1作为AD建立域SQL2012.TEST。三台虚拟机的名称分别为SQLServerTest1,SQLServerTest2,SQLServerTest3。

 

配置Windows Server

首先在分别在三台测试服务器上安装故障转移集群的功能,如图2所示。

Windows_failover_cluster

图2.在测试服务器上安装故障转移集群的功能

 

在安装好故障转移集群的功能之后,在Server1上进行对集群的配置.如图3所示。

2

图3.在故障转移集群管理中创建集群

 

然后在接下来的步骤中,将三台服务器加入集群,如图4所示。

WindowsCluster1

图4.将三台测试服务器加入集群

 

点击下一步,进行测试,如图5所示。

WindowsCluster2

WindowsCluster3

图5.对集群进行验证测试

 

点击下一步,添加集群名称,如图6所示。

WindowsCluster4

图6.添加集群IP和集群名称

 

然后点击下一步确认后,最后完成集群的创建,如图7所示。

WindowsCluster7

图7.完成集群的创建

 

配置SQL Server

在配置完Windows Server之后,就该配置SQL SERVER了。分别在三台测试机上安装SQL Server 2012,所安装的功能如图8所示。

SetupComponent

图8.SQL Server 2012安装的功能

 

安装完SQL Server 2012之后,运行SQL Server配置管理器,如图9所示。

3

图9.运行SQL Server 配置管理器

然后在SQL Server实例中开启alwaysOn选项,如图10所示.

4

图10.开启AlwaysOn

 

 

未完待续….

Sql Server2008-读写分离 - 孤狼灬 - 博客园

mikel阅读(742)

来源: Sql Server2008-读写分离 – 孤狼灬 – 博客园

SQL Server提供了多种读写分离的机制,这里只介绍下发布和订阅的方式,因为博主只用过这种形式,SQL Server 2012中新增的功能Always On也可以实现。
快照发布
快照复制是完全按照数据和数据库对象出现时的状态来复制和分发它们的过程。快照复制不需要连续地监控数据变化,因为已发布数据的变化不被增量地传播到订阅服务器,而是周期性的被一次复制。
a.本地发布:要启动SQL Server代理
1.MyBookShop是主数据库,MyBookShop_R是被同步的数据库既辅数据库,现在可以看出主数据库比辅数据库多两条数据,现在我们把这两条数据通过发布快照的方式同步过去。
2.
3.
4.
5.
6.可以在“计划在下时间运行快照代理”中设置运行时间,快照并非实时同步更新数据库,所以可以设置个时间,也可以不设置,只同步一次就行。
7.点击安全设置
8.
9.
10.
11.
12.
13.创建成功
b.本地订阅
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.刷新显示订阅
16.重新查询数据库,看看有没有同步过去
注:如果主数据库添加数据,辅数据库不会实时同步过去,除非重新启动代理或者到了作业执行的时间才会再次同步,如果想要数据实时同步[说是实时也会有几秒延迟,没有绝对的实时同步]到辅数据库就用事物发布而非快照发布
事务发布
事务发布的方法和快照发布基本相同,事务发布可以实时更新数据,当添加、修改或者删除一条数据时,主数据库上的变动会实时同步到辅数据库【有几秒延迟】;
事务发布可以实现读写分离,主库发布事务,多台辅库订阅,辅库要设置成只读模式;

.Net微服务实战之技术架构分层篇 - 陈珙 - 博客园

mikel阅读(1204)

来源: .Net微服务实战之技术架构分层篇 – 陈珙 – 博客园

一拍即合

上一篇《.Net微服务实战之技术选型篇》,从技术选型角度讲解了微服务实施的中间件的选择与协作,工欲善其事,必先利其器,中间件的选择是作为微服务的基础与开始,也希望给一直想在.Net入门微服务的同行有一个很好的方向。在此篇重新整理了一下整个微服务项目的demo,希望对有需要的朋友起到一定的帮助:https://github.com/SkyChenSky/Sikiro

那么我在公司实施微服务的时候,也不是一拍脑袋想上就上的。刚入职公司的时候才3、4个人,产品给到我的规划只有一个很简单的系统,包含权限、客服IM、内容管理三个模块,我当时想着优先证明我们的开发能力和效率,于是使用简单的单体架构不到三个星期项目就完成了。产品在我们开发的期间把整个项目的规划和平台系统的划分给梳理了一遍,终于让我有一个很明确的技术实施方向,同时公司的人力成本预算也批了下来开始进行团队扩招。

于是我与老领导商量了一下,在现在这个情况,无论业务还是团队都具有使用微服务架构的可操作性,再采用部分DevOps的思想给与微服务实施的支持,能顺利的实施落地微服务问题不大。我们俩讨论了一番,我有良好的微服务技术储备,他有很好的运维支撑,就这样咱两达成了共识。于是我着手翻出了收藏已久的微服务中间件、架构分层、服务拆分的资料,从此开始了我的微服务实施之路。

PS:我们讨论实施微服务的时候除了以上冠冕堂皇的理由之外,其实还存有一点私心,就是现在企业招聘很多需要有实施微服务经验的人才,但是80%的项目和同行又是没有这样的实施必要与经验,这就是鸡生蛋和蛋生鸡的问题。我毫无隐瞒的说出我们的私心并不是怂恿大家冒着风险去实施,而是希望大家通过分析现在团队的组织架构、技术储备、业务架构,在条件允许的情况下满足您的小小要求,微服务虽不是银弹,但我们也需要成长。

架构思维

抽象是作为架构思维的核心,使我们站在大局观察,屏蔽细节;这系统划分哪几个模块?模块之间的如何协作的?抽象又可以衍生出两种思想划分与协作。

  划分的目的是为了定责与拆分,定责不是交通事故的定责而是划定职责,明确模块的使用场景,应该被什么依赖?应该依赖什么?拆分其实就是分而治之的思想,把一个复杂的大问题拆分成一个个简单而小的问题,化繁为简逐个击破自然就迎刃而解。

协作的目的是整合划分好的模块,被拆分的模块如果无法整合到一起,拆分则失去了他原有的意义。

不谋而合

技术服务于架构,架构服务于业务,业务服务于商务。所以有明确的业务蓝图才可以很好的规划架构方向;选择好合适的技术才能很好的支撑架构。此时我们开始着手实施微服务,然而在实施时我们还会考虑一个比较核心点,究竟如何微?粒度究竟到什么程度?怎么明确依赖关系?大家或多或少都会听说身边同行有实施微服务的失败案例:拆分粒度过细导致系统复杂度过高;拆分粒度太粗又没达到微服务该有的效果等。那么是否在业界有一套科学的指导方法论?我认为是有的,DDD战略设计分层架构。

  埃里克、埃文斯在2004年发表了《领域驱动设计》一书的,此后一直是雷声大雨点小,在2014年软件教父马丁花给微服务一个全面描述,让它走向一个高潮后,DDD终于赢来了他的春天。为什么说DDD适合微服务呢?DDD是一种通过划分业务边界,将复杂的业务领域简单化的设计思想,也就是化繁为简。为什么在上文重点强调DDD战略设计?DDD分为战略设计战术设计。

战略设计

主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的界限上下文,界限上下文可以作为微服务设计的参考边界。

战术设计

主要从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,例如我们常讨论的聚合根、实体、值对象、领域服务等代码逻辑的设计与实现。

从以上两点的描述可以看出,战略设计从业务视角出发,而架构服务于业务,两者都需要从业务出发,DDD战略设计微服务都有同样的设计思想:分而治之、化繁为简,那么战略设计的思想完全可以作为微服务架构设计的指导思想,此时此刻此场景不谋而合。

分层&切割

也可以叫N层架构(N>=2),其实本质在于划分职责、隔离关注点,保证各层之间的差异足够清晰,边界足够明显,其特点自顶向下依赖,逐层传递。

横向拆分(横向分层)

首先我按照分层架构的思想以纵向维度拆分,通俗点就是按照自定向下的按照各层职责进行分解,主要共分5层,UI层、聚合API服务层、基础业务API服务层、基础设施层、数据库层

调用链路自顶往下,用户–>UI–>API网关–>聚合API服务–>Consul+Consul Template+Nginx–>业务API服务–>数据库

  UI层

依赖于聚合API服务层,操作与接口11对应,主要负责可见即可得的工作:数据展示、交互动画等。

  入站API网关

主要负责聚合API服务层内外网隔离、入站规则控制,防止外部大流量冲垮内部服务。

  聚合API服务层

UI层依赖,依赖于基础业务API服务层,主要负责基础业务API服务层的接口的逻辑组合,不直连数据库,可通过API网关暴露给UI层调用。

  注册服务中心

记录基础业务API服务层的服务IP列表,内网使用,衔接聚合API服务层基础业务API服务层

  基础业务API服务层,

聚合API服务层依赖,依赖于数据库层,可做具体的数据库读写处理,内网使用,同层服务之间不互相依赖引用。

  数据库层

包括非关系型数据库与关系型数据库。

  基础设施服务层

可被所有层都依赖,如果被UI层依赖则通过API网关暴露,如果被内网服务依赖则通过注册发现,可直连数据库。

  出站API网关

主要负责基础设施服务层内外网隔离,转发第三方开放API请求,出站规则控制,防止被无法把控的第三方服务而拖垮内部服务。

 纵向拆分(纵向切割)

接下来,我们可以通过DDD进行服务的切割,通俗点描述就是将同一个较大的服务拆解成为多个较小的服务。

那么究竟要根据什么样的信息和过程进行拆解呢?有一个工作坊叫作”事件风暴“,事件风暴是一个从拆解到整合的过程,过程中需要领域专家(需求提出者)技术实践者协作完成领域建模。

一般采用场景分析用例分析尽可能分解出领域对象(实体、命令、事件),可以从交流的过程中提取出领域专家(需求提出者)口中的名词、动作、触发事件等,这是一个拆解的过程。

将以上沟通后的结果进行重新梳理,寻找他们的关系进行汇聚,形成聚合与界限上下文,这就是一个整合的过程。

一个微服务粒度可以粗与界限上下文一致,粒度可以细化到一个聚合。

举个例子:

我们平台拥有三种不同业务领域的系统:客户中心、企业管理系统、内部管理系统

那么,聚合API服务层则拥有客户系统API服务、企业管理系统API服务,内部管理系统API服务。

  客户中心的拥有客户信息管理、支付、订单管理等业务模块。

企业管理系统拥有订单管理、权限管理、支付、仓储等业务模块。

内部管理系统拥有权限管理、报表、账户管理等业务模块。

所有系统涉及到自定义订单号、消息推送等业务。

从以上得知,核心域包括仓储、订单业务、客户信息。通用域包括权限管理、账户认证、支付模块、消息推送等。支撑域包括自定义订单号。

因此基础业务API层可以划分:仓储API服务、订单API服务、客户API服务、权限API服务、认证API服务,支付API服务。

基础设施API层可以划分:ID发号API服务,消息推送API服务。

后来多次跟产品经理沟通后得知,仓储服务在某个场景下需要把修改订单服务的状态,那么这里有个触发事件,而且是跨微服务的,因此引入了基于消息的最终一致性的分布式事务进行解决。

如果随着业务继续扩大,团队人数增多,则可以更加的细分,例如仓储拆分成快运、集运等。支付拆分成微信支付、支付宝等。

 项目示例

上一篇《.Net微服务实战之技术选型篇》我整理了我们公司使用的框架开源到了github,这次我拿了部分业务项目作为示例并上传了。

https://github.com/SkyChenSky/Sikiro

首先想说明几点:

1.这个不是标准,只是针对我们公司情况取舍后的结果,每个公司的业务有复杂有简单大家视情况完善自己的项目。

2.为了保护公司原有的业务隐私,我做了部分逻辑的删除,所以大家如果看到不完整的逻辑是正常现象。

3.希望大家把思维放高,不要死抠细节,求同存异。

  4.代码在原有的基础上修改了名称和引用路径会有变化,如果有问题随时在评论和github反馈给我。

作  者: 陈珙
出  处:http://www.cnblogs.com/skychen1218/
关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是作者坚持原创和持续写作的最大动力!

.Net微服务实战之Kubernetes的搭建与使用 - 陈珙 - 博客园

mikel阅读(1243)

来源: .Net微服务实战之Kubernetes的搭建与使用 – 陈珙 – 博客园

系列文章

前言

说到微服务就得扯到自动化运维,然后别人就不得不问你用没用上K8S。无论是概念上还是在实施搭建时,K8S的门槛比Docker Compose、Docker Swarm高了不少。我自己也经过了多次的实践,整理出一套顺利部署的流程。

我这次搭建花了一共整整4个工作实践与一个工作日写博客,中间有一个网络问题导致reset了集群重新搭了一次,完成后结合了Jenkins使用,还是成就感满满的。如果对大家有用,还请点个推荐与关注。

基本概念

Kubectl

kubectl用于运行Kubernetes集群命令的管理工具,Kubernetes kubectl 与 Docker 命令关系可以查看这里

http://docs.kubernetes.org.cn/70.html

Kubeadm

kubeadm 是 kubernetes 的集群安装工具,能够快速安装 kubernetes 集群,相关命令有以下:

kubeadm init

kubeadm join

Kubelet

kubelet是主要的节点代理,它会监视已分配给节点的pod,具体功能:

  • 安装Pod所需的volume。
  • 下载Pod的Secrets。
  • Pod中运行的 docker(或experimentally,rkt)容器。
  • 定期执行容器健康检查。

Pod

Pod是Kubernetes创建或部署的最小(最简单)的基本单位,一个Pod代表集群上正在运行的一个进程,它可能由单个容器或多个容器共享组成的资源。

一个Pod封装一个应用容器(也可以有多个容器),存储资源、一个独立的网络IP以及管理控制容器运行方式的策略选项。

Pods提供两种共享资源:网络和存储。

网络

每个Pod被分配一个独立的IP地址,Pod中的每个容器共享网络命名空间,包括IP地址和网络端口。Pod内的容器可以使用localhost相互通信。当Pod中的容器与Pod 外部通信时,他们必须协调如何使用共享网络资源(如端口)。

存储

Pod可以指定一组共享存储volumes。Pod中的所有容器都可以访问共享volumes,允许这些容器共享数据。volumes 还用于Pod中的数据持久化,以防其中一个容器需要重新启动而丢失数据。

Service

一个应用服务在Kubernetes中可能会有一个或多个Pod,每个Pod的IP地址由网络组件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象。

Kubernetes ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。

Type 的取值以及行为如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。
  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

其他详细的概念请移步到 http://docs.kubernetes.org.cn/227.html

物理部署图

 

Docker-ce 1.19安装

在所有需要用到kubernetes服务器上安装docker-ce

卸载旧版本 docker

yum remove docker docker-common docker-selinux dockesr-engine -y
升级系统软件
yum upgrade -y
安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
添加docker-ce软件源
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新并安装 docker-ce
yum makecache fast
yum install docker-ce-19.03.12 -y
添加docker国内镜像源
vim /etc/docker/daemon.json
 
{
"exec-opts": ["native.cgroupdriver=systemd"],
"registry-mirrors" : [
    "http://ovfftd6p.mirror.aliyuncs.com",
    "http://registry.docker-cn.com",
    "http://docker.mirrors.ustc.edu.cn",
    "http://hub-mirror.c.163.com"
  ],
  "insecure-registries" : [
    "registry.docker-cn.com",
    "docker.mirrors.ustc.edu.cn"
  ],
  "debug" : true,
  "experimental" : true
}

启动服务

systemctl start docker
systemctl enable docker

安装kubernetes-1.18.3

所有需要用到kubernetes的服务器都执行以下指令。

添加阿里kubernetes源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
安装并启动
yum install kubeadm-1.18.3 kubectl-1.18.3 kubelet-1.18.3

启动kubelet

systemctl enable kubelet
systemctl start kubelet

在Master设置环境变量,在/etc/profile中配置
vim /etc/profile
在最后添加如下配置
export KUBECONFIG=/etc/kubernetes/admin.conf

执行命令使其起效

source /etc/profile

初始化k8s集群

在master节点(server-a)进行初始化集群

开放端口

firewall-cmd --permanent --zone=public --add-port=6443/tcp
firewall-cmd --permanent --zone=public --add-port=10250/tcp
firewall-cmd --reload
关闭swap
vim /etc/fstab
#注释swap那行
 
swapoff -a

设置iptables规则

echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
echo 1 > /proc/sys/net/bridge/bridge-nf-call-ip6tables

初始化

kubeadm init --kubernetes-version=1.18.3  --apiserver-advertise-address=192.168.88.138   --image-repository registry.aliyuncs.com/google_containers  --service-cidr=10.10.0.0/16 --pod-network-cidr=10.122.0.0/16 --ignore-preflight-errors=Swap

pod-network-cidr参数的为pod网段:,apiserver-advertise-address参数为本机IP。

  如果中途执行有异常可以通过 kubeadm reset 后重新init。
初始化成功执行下面指令
 mkdir -p $HOME/.kube
 sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
 sudo chown $(id -u):$(id -g) $HOME/.kube/config
查看node和pod信息
kubectl get node
kubectl get pod --all-namespaces

安装flannel组件

在master节点(server-a)安装flannel组件

找个梯子下载kube-flannel.yml文件

https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

下载不了也没关系,我复制给到大家:

 View Code

先拉取依赖镜像

 docker pull  quay.io/coreos/flannel:v0.12.0-amd64

把上面文件保存到服务器然后执行下面命令

kubectl apply -f kube-flannel.yml

安装dashboard

在master节点(server-a)安装dashboard组件

继续用梯子下载recommended.yml文件

https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.3/aio/deploy/recommended.yaml

没梯子的可以复制下方原文件

 View Code
第39行修改,端口范围30000-32767
spec:
  type: NodePort
  ports:
    - port: 443
      targetPort: 8443
      nodePort: 30221
  selector:
    k8s-app: kubernetes-dashboard

第137行开始,修改账户权限,主要三个参数,kind: ClusterRoleBinding,roleRef-kind: ClusterRole,roleRef-name: cluster-admin

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard

---

保存到服务器后执行以下命令

kubectl apply -f recommended.yaml

等待一段时间启动成功后,https://ip+nodePort,查看UI

Token通过下面指令获取

kubectl -n kubernetes-dashboard get secret

kubectl describe secrets -n kubernetes-dashboard kubernetes-dashboard-token-kfcp2  | grep token | awk 'NR==3{print $2}'

加入Worker节点

在server-b与server-c执行下面操作

把上面init后的那句join拷贝过来,如果忘记了可以在master节点执行下面指令:

kubeadm token list

openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

通过返回的数据拼装成下面指令

kubeadm join 192.168.88.138:6443 --token 2zebwy.1549suwrkkven7ow  --discovery-token-ca-cert-hash sha256:c61af74d6e4ba1871eceaef4e769d14a20a86c9276ac0899f8ec6b08b89f532b

查看节点信息

kubectl get node

部署Web应用

在master节点(sever-a)执行下面操作

部署应用前建议有需要的朋友到【.Net微服务实战之CI/CD】看看如何搭建docker私有仓库,后面需要用到,搭建后私有库后执行下面指令

kubectl create secret docker-registry docker-registry-secret --docker-server=192.168.88.141:6000 --docker-username=admin --docker-password=123456789

docker-server就是docker私有仓库的地址

下面是yaml模板,注意imagePullSecrets-name与上面的命名的一致,其余的可以查看yaml里的注释

 View Code

把yaml文件保存到服务器后执行下面命令

kubectl create -f testdockerswarm.yml

整个搭建部署的过程基本上到这里结束了。

访问

可以通过指令kubectl get service得到ClusterIP,分别在server-c和sever-b执行curl 10.10.184.184

也可以通过执行kubectl get pods -o wide得到pod ip,在server-c执行curl 10.122.2.5 和 server-b执行curl 10.122.1.7

也可以在外部访问 server-c和server-b的 ip + 31221

如果节点有异常可以通过下面指令排查

journalctl -f -u kubelet.service | grep -i error -C 500

如果Pod无法正常running可以通过下面指令查看

kubectl describe pod testdockerswarm-deployment-7bc647d87d-qwvzm

 

作  者: 陈珙
出  处:http://www.cnblogs.com/skychen1218/
关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是作者坚持原创和持续写作的最大动力!

.NET Core-全局性能诊断工具 - chaney1992 - 博客园

mikel阅读(1030)

来源: .NET Core-全局性能诊断工具 – chaney1992 – 博客园

前言:

现在.NET Core 上线后,不可避免的会出现各种问题,如内存泄漏、CPU占用高、接口处理耗时较长等问题。这个时候就需要快速准确的定位问题,并解决。

这时候就可以使用.NET Core 为开发人员提供了一系列功能强大的诊断工具。

接下来就详细了解下:.NET Core 全局诊断工具

  • dotnet-counters
  • dotnet-dump
  • dotnet-gcdump
  • dotnet-trace
  • dotnet-symbol
  • dotnet-sos

1、dotnet-counters:

简介:dotnet-counters 是一个性能监视工具,用于初级运行状况监视和性能调查。 它通过 EventCounter API 观察已发布的性能计数器值。例如,可以快速监视CUP使用情况或.NET Core 应用程序中的异常率等指标

 安装:通过nuget包安装:

dotnet tool install --global dotnet-counters

 主要命令: 

  • dotnet-counters ps
  • dotnet-counters list
  • dotnet-counters collect
  • dotnet-counters monitor

a)dotnet-counters ps显示可监视的 dotnet 进程的列表

b)dotnet-counters list命令:显示按提供程序分组的计数器名称和说明的列表

  

包括:运行时和Web主机运行信息

 c)dotnet-counters collect 命令:定期收集所选计数器的值,并将它们导出为指定的文件格式

dotnet-counters collect [-h|--help] [-p|--process-id] [-n|--name] [--diagnostic-port] [--refresh-interval] [--counters <COUNTERS>] [--format] [-o|--output] [-- <command>]

参数说明:

示例:收集dotnet core 服务端所有性能计数器值,间隔时间为3s

 d)dotnet-counters monitor命令:显示所选计数器的定期刷新值

dotnet-counters monitor [-h|--help] [-p|--process-id] [-n|--name] [--diagnostic-port] [--refresh-interval] [--counters] [-- <command>]

示例: dotnet-counters monitor –process-id 18832 –refresh-interval 2

2、dotnet-dump:

简介:通过 dotnet-dump 工具,可在不使用本机调试器的情况下收集和分析 Windows 和 Linux 核心转储。
安装:

dotnet tool install --global dotnet-dump

命令:

  • dotnet-dump collect
  • dotnet-dump analyze

 a) dotnet-dump collect:从进程生成dump

dotnet-dump collect [-h|--help] [-p|--process-id] [-n|--name] [--type] [-o|--output] [--diag]

参数说明:

-h|–help 显示命令行帮助。
-p|–process-id <PID> 指定从中收集转储的进程的 ID 号。
-n|–name <name> 指定从中收集转储的进程的名称。
–type <Full|Heap|Mini> 指定转储类型,它确定从进程收集的信息的类型。 有三种类型:
Full – 最大的转储,包含所有内存(包括模块映像)。
Heap – 大型且相对全面的转储,其中包含模块列表、线程列表、所有堆栈、异常信息、句柄信息和除映射图像以外的所有内存。
Mini – 小型转储,其中包含模块列表、线程列表、异常信息和所有堆栈
-o|–output <output_dump_path> 应在其中写入收集的转储的完整路径和文件名。
如果未指定:
在 Windows 上默认为 .\dump_YYYYMMDD_HHMMSS.dmp 。
在 Linux 上默认为 ./core_YYYYMMDD_HHMMSS 。
YYYYMMDD 为年/月/日,HHMMSS 为小时/分钟/秒。
–diag 启用转储收集诊断日志记录。

示例:dotnet-dump collect -p 18832

 b)dotnet-dump analyze:启动交互式 shell 以了解转储

dotnet-dump analyze <dump_path> [-h|--help] [-c|--command]

  示例:dotnet-dump analyze dump_20210509_193133.dmp  进入dmp分析,查看堆栈和未处理异常

   

Sos命令列表:

命令 函数
soshelp 显示所有可用命令
soshelp|help <command> 执行指定的命令。
exit|quit 退出交互模式。
clrstack <arguments> 仅提供托管代码的堆栈跟踪。
clrthreads <arguments> 列出正在运行的托管线程。
dumpasync <arguments> 显示有关垃圾回收堆上异步状态机的信息。
dumpassembly <arguments> 显示有关指定地址处程序集的详细信息。
dumpclass <arguments> 显示有关指定地址处的 EEClass 结构的信息。
dumpdelegate <arguments> 显示有关指定地址处的委托的信息。
dumpdomain <arguments> 显示所有 AppDomain 和指定域中的所有程序集的信息。
dumpheap <arguments> 显示有关垃圾回收堆的信息和有关对象的收集统计信息。
dumpil <arguments> 显示与托管方法关联的 Microsoft 中间语言 (MSIL)。
dumplog <arguments> 将内存中压力日志的内容写入到指定文件。
dumpmd <arguments> 显示有关指定地址处的 MethodDesc 结构的信息。
dumpmodule <arguments> 显示有关指定地址处的模块的信息。
dumpmt <arguments> 显示有关指定地址处的 MethodTable 的信息。
dumpobj <arguments> 显示有关位于指定地址处的对象的信息。
dso|dumpstackobjects <arguments> 显示在当前堆栈的边界内找到的所有托管对象。
eeheap <arguments> 显示有关内部运行时数据结构所使用的进程内存的信息。
finalizequeue <arguments> 显示所有已进行终结注册的对象。
gcroot <arguments> 显示有关对指定地址处的对象的引用(或根)的信息。
gcwhere <arguments> 显示传入参数在 GC 堆中的位置。
ip2md <arguments> 显示 JIT 代码中指定地址处的 MethodDesc 结构。
histclear <arguments> 释放由 hist* 命令系列使用的任何资源。
histinit <arguments> 从保存在调试对象中的压力日志初始化 SOS 结构。
histobj <arguments> 显示与 <arguments> 相关的垃圾回收压力日志重定位。
histobjfind <arguments> 显示在指定地址处引用对象的所有日志项。
histroot <arguments> 显示与指定根的提升和重定位相关的信息。
lm|modules 显示进程中的本机模块。
name2ee <arguments> 显示 <argument> 的 MethodTable 和 EEClass 结构。
pe|printexception <arguments> 显示从 Exception 类派生的 <argument> 的任何对象。
setsymbolserver <arguments> 启用符号服务器支持
syncblk <arguments> 显示 SyncBlock 持有者信息。
threads|setthread <threadid> 设置或显示 SOS 命令的当前线程 ID。

3、dotnet-gcdump:

简介:dotnet-gcdump 工具可用于为活动 .NET 进程收集 GC(垃圾回收器)转储。

dotnet-gcdump 全局工具使用 EventPipe 收集实时 .NET 进程的 GC(垃圾回收器)转储。 创建 GC 转储时需要在目标进程中触发 GC、开启特殊事件并从事件流中重新生成对象根图。 此过程允许在进程运行时以最小的开销收集 GC 转储。 

  这些转储对于以下几种情况非常有用:

  • 比较多个时间点堆上的对象数。
  • 分析对象的根(回答诸如“还有哪些引用此类型的内容?”等问题)。
  • 收集有关堆上的对象计数的常规统计信息。

安装:

dotnet tool install --global dotnet-gcdump

 示例:从当前正在运行的进程中收集 GC 转储

dotnet-gcdump collect [-h|--help] [-p|--process-id <pid>] [-o|--output <gcdump-file-path>] [-v|--verbose] [-t|--timeout <timeout>] [-n|--name <name>]

参数说明:

参数 说明:
-h|–help 显示命令行帮助。
-p|–process-id <pid> 可从中收集 GC 转储的进程 ID。
-o|–output <gcdump-file-path> 应写入收集 GC 转储的路径。 默认为 .\YYYYMMDD_HHMMSS_<pid>.gcdump。
-v|–verbose 收集 GC 转储时输出日志。
-t|–timeout <timeout> 如果收集 GC 转储的时间超过了此秒数,则放弃收集。 默认值为 30。
-n|–name <name> 可从中收集 GC 转储的进程的名称。

生成示例:dotnet-gcdump collect -p 18832

查看生成文件:使用perfview查看:

4、dotnet-trace:

简介:分析数据通过 .NET Core 中的 EventPipe 公开。 通过 dotnet-trace 工具,可以使用来自应用的有意思的分析数据,这些数据可帮助你分析应用运行缓慢的根本原因。
安装:

dotnet tool install --global dotnet-trace

命令:

dotnet-trace [-h, --help] [--version] <command>

常用命令:

 

命令 说明
dotnet-trace collect 从正在运行的进程中收集诊断跟踪,或者启动子进程并对其进行跟踪(仅限 .NET 5+)。 若要让工具运行子进程并自其启动时对其进行跟踪,请将 -- 追加到 collect 命令。
dotnet-trace convert 将 nettrace 跟踪转换为备用格式,以便用于备用跟踪分析工具。
dotnet-trace ps 列出可从中收集跟踪的 dotnet 进程。
dotnet-trace list-profiles 列出预生成的跟踪配置文件,并描述每个配置文件中包含的提供程序和筛选器。

 示例:收集进程18832诊断跟踪:

  

使用Vs打开生成的跟踪文件如下:

5、dotnet-symbol:

简介:dotnet-symbol 用于下载打开核心转储或小型转储所需的文件(符号、DAC/DBI、主机文件等)。 如果需要使用符号和模块来调试在其他计算机上捕获的转储文件,请使用此工具
安装:

dotnet tool install --global dotnet-symbol

命令:

dotnet-symbol [-h|--help] [options] <FILES>

options:

参数 说明
–microsoft-symbol-server 添加“http://msdl.microsoft.com/download/symbols”符号服务器路径(默认)。
–server-path <symbol server path>  将符号服务器添加到服务器路径。
authenticated-server-path <pat> <server path> 使用个人访问令牌 (PAT) 将经过身份验证的符号服务器添加到服务器路径。
–cache-directory <file cache directory> 添加缓存目录。
–recurse-subdirectories 处理所有子目录中的输入文件。
–host-only 仅下载 lldb 加载核心转储所需的主机程序(即 dotnet)。
–symbols 下载符号文件(.pdb、.dbg 和 .dwarf)。
–modules 下载模块文件(.dll、.so 和 .dylib)。
Debugging 下载特殊的调试模块(DAC、DBI 和 SOS)。
–windows-pdbs 当可移植的 PDB 也可用时,会强制下载 Windows PDB。
-o, –output <output directory> 设置输出目录。 否则,请在输入文件旁边写入(默认)。
-d, –diagnostics 启用诊断输出。
-h|–help 显示命令行帮助。

6、dotnet-sos:

简介:dotnet-sos 在 Linux 和 macOS(如果使用的是 Windbg/cdb,则在 Windows 上)安装 SOS调试扩展

安装:

dotnet tool install --global dotnet-sos

命令:在本地安装用于调试 .NET Core 进程的 SOS 扩展

dotnet-sos install

示例:

总结:

微软提供了一套强大的诊断工具,熟练的使用这些工具,可以更快更有效的发现程序的运行问题,解决程序的性能问题。

过程中主要使用:counters、dump、trace 工具用于分析.NET Core性能问题。

最近又了解到微软已对这些基础工具已封装了对应包(Microsoft.Diagnostics.NETCore.Client),可以用来开发出自己的有界面的诊断工具。后续将了解实现一个。

参考文档:

https://docs.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters

GitHub – dotnet/diagnostics: This repository contains the source code for various .NET Core runtime diagnostic tools and documents.

https://channel9.msdn.com/Shows/On-NET/Introducing-the-Diagnostics-Client-Library-for-NET-Core

.NET Core with 微服务 - 什么是微服务 - Agile.Zhou - 博客园

mikel阅读(888)

来源: .NET Core with 微服务 – 什么是微服务 – Agile.Zhou – 博客园

微服务是这几年最流行的架构,说起架构不提微服务都不好意思跟人家打招呼。最近想要再梳理一下关于微服务的知识,并且结合本人的一些实践经验来做一些总结与分享。前面会分享一些概念性的东西,后面也会使用.net来实践,一步步完成一个简单的微服务架构的小demo。

什么是微服务

其实微服务并没有统一的标准定义。微服务是一种软件架构的风格。它首先由大神martin fowler提出,2014年3月25号在他的博客上发表了一篇博客来描述了这种微服务的架构。原文地址(https://www.martinfowler.com/articles/microservices.html)。
相对于传统的单体(Monolithic)架构应用,微服务把单个进程的应用拆分为多个单独部署的服务。每个服务对外提供一些接口来进行服务间的通讯或者对第三方提供功能。每个独立的服务甚至使用自己独立的存储技术,独立的语言技术栈。说到底微服务架构还是贯彻了软件开发中:单一职责、分而治之、解耦等基本理念,只是它把这种理念从类、类库级别提升到了进程级别。

图片引用自https://www.redhat.com/zh/topics/microservices/what-are-microservices

微服务与SOA

微服务架构看起来跟SOA架构非常相似。事实上微服务架构就是SOA的一种现代化的实现方式,一次进化。虽然不能在两者之间画等号,但是他们的思想确实是一致的。

图片引用自https://zato.io/docs/intro/esb-soa-cn.html

微服务与SOA之间的区别网上有很多,在此不再大段的复制黏贴网上现成的文字,简单谈谈自己的一些理解。
首先SOA大多数情况下是作用于企业内部,它通过ESB等总线技术把企业内的服务(或者称之为应用)串联起来。SOA虽然是在解耦、去中心化,但是它通常跟某种ESB技术强耦合起来,以至于ESB会成为那个最大的中心。微服务的作用范围是应用而不是庞大的企业。微服务不在依赖ESB等总线技术,服务间的通讯通过无状态、轻量级的接口实现。协议采用http、json等通用协议无关开发语言,谁都可以调用。所以相比SOA有更好的去中心化意义。

优点

上面说了这么多关于微服务的知识,那么实施微服务到底为我们带来了哪些好处?网上有很多复制黏贴的话其实我不太苟同,比如:部署简单,如果没有强大的运维团队微服务的部署显然是比传统单体应用部署难度更大了。 比如快速开发快速迭代:事实上单体应用也不用等到完全开发完才能上线。下面说下我认为的微服务的几个优点:

  1. 技术异构
    采用微服务架构可以很方便的在每个服务中使用不同的技术栈。每个团队可以根据自身的业务情况,人员情况安排使用最合适的技术。如果我们服务业务是AI那就考虑pyhton,如果我们的人员比较熟悉JavaScript,那么可以选nodejs。当然技术的多样性也是要权衡的,不能说每个服务都撸一种语言每个都试验一把,这样未必就是好事情了。
  2. 扩展性
    当我们的业务做的越来越大,流量越来越大的时候,需要对计算资源进行扩展。相对于单体应用,微服务可以更好的进行扩展。传统单体应用水平扩展的时候可能需要把整个应用都扩展多个实例。事实上我们的业务越来越大的时候,往往只是某个模块压力巨大。而采用微服务架构我们只需要对某压力大的服务进行水平扩展。配合现在的容器化技术能够更好的利用技术资源。
  3. 可靠性
    由于每个服务都是独立部署,当某个服务故障的时候通常不会导致其它服务同时故障,只是丧失了部分能力。再配合服务降级、熔断等技术可以比单体应用提供更好的可靠性。
  4. 强模块化边界
    这个概念在网上很少出现。我是在B站上杨波老师的一个关于微服务视频上看到的,对这个观点比较认同。模块化是我们软件开发常用的模式。原来我们按类、按类库进行模块化,现在通过微服务架构直接把模块服务化了,并且能独立部署运行。其它模块不在需要直接引用相关类库就可以使用它。而且实施微服务架构后会强制团队进行应用的模块化,对模块的边界进行明确的划分。当然模块的边界划分是个技术活,如果划分的不够好那就是场灾难。

缺点

这个世界上的事情都是具有两面性的。微服务除了有其优点,自然也有缺点。我们在做架构的时候要尽量处理好这些缺点,避免踩到巨坑。下面谈谈我对微服务缺点的一些看法。

  1. 运维难度增加
    本来只需要部署一个IIS站点或者Tomcat服务、维护一个数据库,现在变成了需要部署N个不同的服务,N个不同类型的数据库。不同的服务甚至可能分散在不同的服务器上。要使这些服务正常的工作,正常的通讯,还要对其进行监控显然比单体架构时代对运维的考验提高了一个维度。没有强大的运维团队、自动化的运维工具的话微服务实施起来出故障的概率显然会大大增加。
  2. 分布式的挑战
    微服务架构天然就是分布式的。但是分布式系统会带来很多单体架构没有的问题。比如分布式事务,数据一致性问题。本来在进程内一个锁或者在数据库开一个事务就能解决的事情,现在不得不借助分布式锁、分布式事务、数据最终一致性来处理。这些问题对开发人员写代码的时候也是很大的挑战。除了一致性的问题,微服务架构中服务之间的通信也会有很高的成本。本来进程内的方法调用变成了跨进程、跨服务的通讯。我们知道网络是不可靠的,出现故障的概率远远超过进程内调用。
  3. 调试,测试难度增加
    由于服务之间互相依赖,在做集成测试或者调试的时候需要把所有依赖的服务、数据库等全部都跑起来。出现问题很难一次性定位到确切位置。由于服务器之间网络带宽的原因多次测试结果可能会有变动,测试的结果不稳定。
  4. 沟通成本提高
    在采用微服务架构开发之后,团队的组织架构都可能跟着变动,团队免不了被拆分成多个小团队甚至不同部门。在公司呆过的都知道,跨团队跨部门之间沟通的成本有多大。本来一天就能修复的bug,很可能变成一周。
  5. 模块划分困难
    我们前面说微服务把每个模块进行独立部署,采用独立的数据库。这么轻描淡写的一句话,事实上实施起来并没有那么容易。如果模块划分的不好,那么会出现非常多的跨库查询,非常多的跨库事务。本来单体架构上很简单的事情变得无比复杂。本来一句Transaction就你搞定的事情,现在可能需要先团队之间进行沟通,然后互相开接口,再使用分布式事务来完成。模块划分的一个好的方案就是采用DDD的思想进划分,但是事实上能把DDD玩好落地也不是一件容易的事。

微服务不是银弹

微服务这几年火热的很。很多公司、架构师言架构必微服务,好像微服务是包治百病的良药。不管项目大小,项目周期,人员配置,技术实力,一股脑的上微服务。见过3,5人小团队一个月就能开发上线的说要进行微服务改造。这么做怕不是微服务真的香,而是为了充实自己的简历。
微服务不是银弹,正如上面所述,微服务在享受它带来的好处的时候也是有巨大的成本开销的。它会带来组织架构上的变动,人员的变动。它大大的提高了系统的复杂性,给运维、开发、测试、调试都带来巨大的挑战。
在采用微服务架构之前最好先思考一下,真的需要微服务吗?权衡一下微服务带来的利弊再下决定。以我个人的经验来看,市面上绝大多数系统更适合单体架构,或者说没必要一上来就采用微服务架构。真正好的架构是在满足当前需求的前提下快速稳定的上线,并对后面的扩展、改造留好余地,以应对后面业务发展带来的需求进行架构的升级改造。

总结

通过以上这些铺垫我们讲了微服务的概念、微服务有哪些优点、微服务又有哪些缺点给我们带来了哪些方面的挑战。以上是我个人的一些浅薄的理解有可能有遗漏或者有错误,大家可以一起讨论一下。
下一篇将会对微服务架构、微服务使用的常用组件进行详细介绍,敬请期待。
谢谢阅读,帮忙点赞。

RabbitMQ实现延时消息的两种方法 - JavaLank - 博客园

mikel阅读(861)

来源: RabbitMQ实现延时消息的两种方法 – JavaLank – 博客园

 

RabbitMQ实现延时消息的两种方法

1、死信队列

1.1消息什么时候变为死信(dead-letter)

  1. 消息被否定接收,消费者使用basic.reject 或者 basic.nack并且requeue 重回队列属性设为false。
  2. 消息在队列里得时间超过了该消息设置的过期时间(TTL)。
  3. 消息队列到达了它的最大长度,之后再收到的消息。

1.2死信队列的原理

当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,这个exchange就为DLX。因此我们只需要在声明正常的业务队列时添加一个可选的”x-dead-letter-exchange”参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上,我们接着监听这个交换机就可以了。

1.3 代码实现

  1. 引入amqp依赖
  2. 声明交换机,队列
package com.lank.demo.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lank
 * @since 2020/12/14 10:44
 */
@Configuration
public class RabbitmqConfig {

    //死信交换机,队列,路由相关配置
    public static final String DLK_EXCHANGE = "dlk.exchange";
    public static final String DLK_ROUTEKEY = "dlk.routeKey";
    public static final String DLK_QUEUE = "dlk.queue";

    //业务交换机,队列,路由相关配置
    public static final String DEMO_EXCHANGE = "demo.exchange";
    public static final String DEMO_QUEUE = "demo.queue";
    public static final String DEMO_ROUTEKEY = "demo.routeKey";

    //延时插件DelayedMessagePlugin的交换机,队列,路由相关配置
    public static final String DMP_EXCHANGE = "dmp.exchange";
    public static final String DMP_ROUTEKEY = "dmp.routeKey";
    public static final String DMP_QUEUE = "dmp.queue";

    @Bean
    public DirectExchange demoExchange(){
        return new DirectExchange(DEMO_EXCHANGE,true,false);
    }

    @Bean
    public Queue demoQueue(){
        //只需要在声明业务队列时添加x-dead-letter-exchange,值为死信交换机
        Map<String,Object> map = new HashMap<>(1);
        map.put("x-dead-letter-exchange",DLK_EXCHANGE);
        //该参数x-dead-letter-routing-key可以修改该死信的路由key,不设置则使用原消息的路由key
        map.put("x-dead-letter-routing-key",DLK_ROUTEKEY);
        return new Queue(DEMO_QUEUE,true,false,false,map);
    }

    @Bean
    public Binding demoBind(){
        return BindingBuilder.bind(demoQueue()).to(demoExchange()).with(DEMO_ROUTEKEY);
    }

    @Bean
    public DirectExchange dlkExchange(){
        return new DirectExchange(DLK_EXCHANGE,true,false);
    }

    @Bean
    public Queue dlkQueue(){
        return new Queue(DLK_QUEUE,true,false,false);
    }

    @Bean
    public Binding dlkBind(){
        return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(DLK_ROUTEKEY);
    }


    //延迟插件使用
    //1、声明一个类型为x-delayed-message的交换机
    //2、参数添加一个x-delayed-type值为交换机的类型用于路由key的映射
    @Bean
    public CustomExchange dmpExchange(){
        Map<String, Object> arguments = new HashMap<>(1);
        arguments.put("x-delayed-type", "direct");
        return new CustomExchange(DMP_EXCHANGE,"x-delayed-message",true,false,arguments);
    }

    @Bean
    public Queue dmpQueue(){
        return new Queue(DMP_QUEUE,true,false,false);
    }

    @Bean
    public Binding dmpBind(){
        return BindingBuilder.bind(dmpQueue()).to(dmpExchange()).with(DMP_ROUTEKEY).noargs();
    }
    

}
  1. 声明一个类用于发送带过期时间的消息
package com.lank.demo.rabbitmq;

import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author lank
 * @since 2020/12/14 10:33
 */
@Component
@Slf4j
public class MessageSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //使用死信队列发送消息方法封装
    public void send(String message,Integer time){
        String ttl = String.valueOf(time*1000);
        //exchange和routingKey都为业务的就可以,只需要设置消息的过期时间
        rabbitTemplate.convertAndSend(RabbitmqConfig.DEMO_EXCHANGE, RabbitmqConfig.DEMO_ROUTEKEY,message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //设置消息的过期时间,是以毫秒为单位的
                message.getMessageProperties().setExpiration(ttl);
                return message;
            }
        });
        log.info("使用死信队列消息:{}发送成功,过期时间:{}秒。",message,time);
    }

    //使用延迟插件发送消息方法封装
    public void send2(String message,Integer time){
        rabbitTemplate.convertAndSend(RabbitmqConfig.DMP_EXCHANGE, RabbitmqConfig.DMP_ROUTEKEY,message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
            //使用延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
                message.getMessageProperties().setHeader("x-delay",time*1000);
                return message;
            }
        });
        log.info("使用延迟插件发送消息:{}发送成功,过期时间:{}秒。",message,time);
    }
}
  1. 编写一个类用于消费消息
package com.lank.demo.rabbitmq;

import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author lank
 * @since 2020/12/15 15:57
 */

@Component
@Slf4j
public class MessageReceiver {

    @RabbitHandler
    @RabbitListener(queues = RabbitmqConfig.DLK_QUEUE)
    public void onMessage(Message message){
        log.info("使用死信队列,收到消息:{}",new String(message.getBody()));
    }

    @RabbitHandler
    @RabbitListener(queues = RabbitmqConfig.DMP_QUEUE)
    public void onMessage2(Message message){
        log.info("使用延迟插件,收到消息:{}",new String(message.getBody()));
    }
}
  1. 编写Controller调用发送消息方法测试结果
package com.lank.demo.controller;
import com.lank.demo.rabbitmq.MessageSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lank
 * @since 2020/12/14 17:05
 */
@RestController
public class MessageController {

    @Autowired
    public MessageSender messageSender;

    //死信队列controller
    @GetMapping("/send")
    public String send(@RequestParam String msg,Integer time){
        messageSender.send(msg,time);
        return "ok";
    }

    //延迟插件controller
    @GetMapping("/send2")
    public String sendByPlugin(@RequestParam String msg,Integer time){
        messageSender.send2(msg,time);
        return "ok";
    }

}
  1. 配置文件application.properties
server.port=4399
#virtual-host使用默认的/就好,如果需要/demo需自己在控制台添加
spring.rabbitmq.virtual-host=/demo
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
  1. 启动项目,打开rabbitmq控制台,可以看到交换机和队列已经创建好。
    image
    rabbitmq2
  2. 在浏览器中请求http://localhost:4399/send?msg=hello&time=5,从控制台的输出来看,刚好5s后接收到消息。
2020-12-16 22:47:28.071  INFO 13304 --- [nio-4399-exec-1] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:hello发送成功,过期时间:5秒。
2020-12-16 22:47:33.145  INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver           : 使用死信队列,收到消息:hello

1.4死信队列的一个小注意点

当我往死信队列中发送两条不同过期时间的消息时,如果先发送的消息A的过期时间大于后发送的消息B的过期时间时,由于消息的顺序消费,消息B过期后并不会立即重新publish到死信交换机,而是会等到消息A过期后一起被消费。

依次发送两个请求http://localhost:4399/send?msg=消息A&time=30和http://localhost:4399/send?msg=消息B&time=10,消息A先发送,过期时间30S,消息B后发送,过期时间10S,我们想要的结果应该是10S收到消息B,30S后收到消息A,但结果并不是,控制台输出如下:

2020-12-16 22:54:47.339  INFO 13304 --- [nio-4399-exec-5] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:消息A发送成功,过期时间:30秒。
2020-12-16 22:54:54.278  INFO 13304 --- [nio-4399-exec-6] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:消息B发送成功,过期时间:10秒。
2020-12-16 22:55:17.356  INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver           : 使用死信队列,收到消息:消息A
2020-12-16 22:55:17.357  INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver           : 使用死信队列,收到消息:消息B

消息A30S后被成功消费,紧接着消息B被消费。因此当我们使用死信队列时应该注意是否消息的过期时间都是一样的,比如订单超过10分钟未支付修改其状态。如果当一个队列各个消息的过期时间不一致时,使用死信队列就可能达不到延时的作用。这时候我们可以使用延时插件来实现这需求。

2 、延时插件

RabbitMQ Delayed Message Plugin是一个rabbitmq的插件,所以使用前需要安装它,可以参考的GitHub地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

2.1如何实现

  1. 安装好插件后只需要声明一个类型type为”x-delayed-message”的exchange,并且在其可选参数下配置一个key为”x-delayed-typ”,值为交换机类型(topic/direct/fanout)的属性。
  2. 声明一个队列绑定到该交换机
  3. 在发送消息的时候消息的header里添加一个key为”x-delay”,值为过期时间的属性,单位毫秒。
  4. 代码就在上面,配置类为DMP开头的,发送消息的方法为send2()。
  5. 启动后在rabbitmq控制台可以看到一个类型为x-delayed-message的交换机。
    rabbitmq3
    rabbitmq4
  6. 继续在浏览器中发送两个请求http://localhost:4399/send2?msg=消息A&time=30和http://localhost:4399/send2?msg=消息B&time=10,控制台输出如下,不会出现死信队列出现的问题:
2020-12-16 23:31:19.819  INFO 13304 --- [nio-4399-exec-9] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用延迟插件发送消息:消息A发送成功,过期时间:30秒。
2020-12-16 23:31:27.673  INFO 13304 --- [io-4399-exec-10] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用延迟插件发送消息:消息B发送成功,过期时间:10秒。
2020-12-16 23:31:37.833  INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver           : 使用延迟插件,收到消息:消息B
2020-12-16 23:31:49.917  INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver           : 使用延迟插件,收到消息:消息A

死信交换机官网介绍:https://www.rabbitmq.com/dlx.html
延时插件GitHub:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

从零开始搞监控系统(1)——SDK - 咖啡机(K.F.J) - 博客园

mikel阅读(707)

来源: 从零开始搞监控系统(1)——SDK – 咖啡机(K.F.J) – 博客园

目前市面上有许多成熟的前端监控系统,但我们没有选择成品,而是自己动手研发。这里面包括多个原因:

  • 填补H5日志的空白
  • 节约公司费用支出
  • 可灵活地根据业务自定义监控
  • 回溯时间能更长久
  • 反哺运营和产品,从而优化产品质量
  • 一次难得的练兵机会

前端监控地基本目的:了解当前项目实际使用的情况,有哪些异常,在追踪到后,对其进行分析,并提供合适的解决方案。

前端监控地终极目标: 1 分钟感知、5 分钟定位、10 分钟恢复。目前是初版,离该目标还比较遥远。

SDK(采用ES5语法)取名为 shin.js,其作用就是将数据通过 JavaScript 采集起来,统一发送到后台,采集的方式包括监听或劫持原始方法,获取需要上报的数据,并通过 gif 传递数据。

整个系统大致的运行流程如下:

一、异常捕获

异常包括运行时错误、Promise错误、框架错误等。

1)error事件

为 window 注册 error 事件,捕获全局错误,过滤掉与业务无关的错误,例如“Script error.”、JSBridge告警等,还需统一资源载入和运行时错误的数据格式。

// 定义的错误类型码
var ERROR_RUNTIME = "runtime";
var ERROR_SCRIPT = "script";
var ERROR_STYLE = "style";
var ERROR_IMAGE = "image";
var ERROR_AUDIO = "audio";
var ERROR_VIDEO = "video";
var ERROR_PROMISE = "promise";
var ERROR_VUE = "vue";
var ERROR_REACT = "react";
var LOAD_ERROR_TYPE = {
  SCRIPT: ERROR_SCRIPT,
  LINK: ERROR_STYLE,
  IMG: ERROR_IMAGE,
  AUDIO: ERROR_AUDIO,
  VIDEO: ERROR_VIDEO
};
/**
 * 监控异常
 */
window.addEventListener(
  "error",
  function (event) {
    var errorTarget = event.target;
    // 过滤掉与业务无关的错误
    if (event.message === "Script error." || !event.filename) {
      return;
    }
    if (
      errorTarget !== window &&
      errorTarget.nodeName &&
      LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]
    ) {
      handleError(formatLoadError(errorTarget));
    } else {
      handleError(
        formatRuntimerError(
          event.message,
          event.filename,
          event.lineno,
          event.colno,
          event.error
        )
      );
    }
  },
  true //捕获
);
/**
 * 生成 laod 错误日志
 * 需要加载资源的元素
 */
function formatLoadError(errorTarget) {
  return {
    type: LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()],
    desc: errorTarget.baseURI + "@" + (errorTarget.src || errorTarget.href),
    stack: "no stack"
  };
}

2)unhandledrejection事件

为 window 注册 unhandledrejection 事件,捕获未处理的 Promise 错误,当 Promise 被 reject 且没有 reject 处理器时触发。

window.addEventListener(
  "unhandledrejection",
  function (event) {
    //处理响应数据,只抽取重要信息
    var response = event.reason.response;
    //若无响应,则不监控
    if (!response) {
      return;
    }
    var desc = response.request.ajax;
    desc.status = event.reason.status;
    handleError({
      type: ERROR_PROMISE,
      desc: desc
    });
  },
  true
);

Promise 常用于异步通信,例如axios库,当响应异常通信时,就能借助该事件将其捕获,得到的结果如下。

{
  "type": "promise",
  "desc": {
    "response": {
      "data": "Error occured while trying to proxy to: localhost:8000/monitor/performance/statistic",
      "status": 504,
      "statusText": "Gateway Timeout",
      "headers": {
        "connection": "keep-alive",
        "date": "Wed, 24 Mar 2021 07:53:25 GMT",
        "transfer-encoding": "chunked",
        "x-powered-by": "Express"
      },
      "config": {
        "transformRequest": {},
        "transformResponse": {},
        "timeout": 0,
        "xsrfCookieName": "XSRF-TOKEN",
        "xsrfHeaderName": "X-XSRF-TOKEN",
        "maxContentLength": -1,
        "headers": {
          "Accept": "application/json, text/plain, */*",
        },
        "method": "get",
        "url": "/api/monitor/performance/statistic"
      },
      "request": {
        "ajax": {
          "type": "GET",
          "url": "/api/monitor/performance/statistic",
          "status": 504,
          "endBytes": 0,
          "interval": "13.15ms",
          "network": {
            "bandwidth": 0,
            "type": "4G"
          },
          "response": "Error occured while trying to proxy to: localhost:8000/monitor/performance/statistic"
        }
      }
    },
    "status": 504
  },
  "stack": "Error: Gateway Timeout
    at handleError (http://localhost:8000/umi.js:18813:15)"
}

这样就能分析出 500、502、504 等响应码所占通信的比例,当高于日常数量时,就得引起注意,查看是否在哪块逻辑出现了问题。

有一点需要注意,上面的结构中包含响应信息,这是需要对 Error 做些额外扩展的,如下所示。

import fetch from 'axios';
function handleError(errorObj) {
  const { response } = errorObj;
  if (!response) {
    const error = new Error('你的网络有点问题');
    error.response = errorObj;
    error.status = 504;
    throw error;
  }
  const error = new Error(response.statusText);
  error.response = response;
  error.status = response.status;
  throw error;
}
export default function request(url, options) {
  return fetch(url, options)
    .catch(handleError)
    .then((response) => {
      return { data: response.data };
    });
}

公司中有一套项目依赖的是 JQuery 库,因此要监控此处的异常通信,需要做点改造。

好在所有的通信都会请求一个通用函数,那么只要修改此函数的逻辑,就能覆盖到项目中的所有页面。

搜索了API资料,以及研读了 JQuery 中通信的源码后,得出需要声明一个 xhr() 函数,在函数中初始化 XMLHttpRequest 对象,从而才能监控它的实例。

并且在 error 方法中需要手动触发 unhandledrejection 事件。

$.ajax({
  url,
  method,
  data,
  success: (res) => {
    success(res);
  },
  xhr: function () {
    this.current = new XMLHttpRequest();
    return this.current;
  },
  error: function (res) {
    error(res);
    Promise.reject({
      status: res.status,
      response: {
        request: {
          ajax: this.current.ajax
        }
      }
    }).catch((error) => {
      throw error;
    });
  }
});

3)框架错误

框架是指目前流行的React、Vue等,我只对公司目前使用的这两个框架做了监控。

React 需要在项目中创建一个 ErrorBoundary 类,捕获错误。

import React from 'react';
export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    // 将component中的报错发送到后台
    shin && shin.reactError(error, info);
  }
  render() {
    if (this.state.hasError) {
      return null
      // 也可以在出错的component处展示出错信息
      // return <h1>出错了!</h1>;
    }
    return this.props.children;
  }
}

其中 reactError() 方法在组装错误信息。

/**
 * 处理 React 错误(对外)
 */
shin.reactError = function (err, info) {
  handleError({
    type: ERROR_REACT,
    desc: err.toString(),
    stack: info.componentStack
  });
};

如果要对 Vue 进行错误捕获,那么就得重写 Vue.config.errorHandler(),其参数就是 Vue 对象。

/**
 * Vue.js 错误劫持(对外)
 */
shin.vueError = function (vue) {
  var _vueConfigErrorHandler = vue.config.errorHandler;
  vue.config.errorHandler = function (err, vm, info) {
    handleError({
      type: ERROR_VUE,
      desc: err.toString(),   //描述
      stack: err.stack      //堆栈
    });
    // 控制台打印错误
    if (
      typeof console !== "undefined" &&
      typeof console.error !== "undefined"
    ) {
      console.error(err);
    }
    // 执行原始的错误处理程序
    if (typeof _vueConfigErrorHandler === "function") {
      _vueConfigErrorHandler.call(err, vm, info);
    }
  };
};

如果 Vue 是被模块化引入的,那么就得在模块的某个位置调用该方法,因为此时 Vue 不会绑定到 window 中,即不是全局变量。

4)难点

虽然把错误都搜集起来了,但是现代化的前端开发,都会做一次代码合并压缩混淆,也就是说,无法定位错误的真正位置。

为了能转换成源码,就需要引入自动堆栈映射(SourceMap),webpack 默认就带了此功能,只要声明相应地关键字开启即可。

我选择了 devtool: “hidden-source-map”,生成完成的原始代码,并在脚本中隐藏Source Map路径。

//# sourceMappingURL=index.bundle.js.map

在生成映射文件后,就需要让运维配合,编写一个脚本(在发完代码后触发),将这些文件按年月日小时分钟的格式命名(例如 202103041826.js.map),并迁移到指定目录中,用于后期的映射。

之所以没有到秒是因为没必要,在执行发代码的操作时,发布按钮会被锁定,其他人无法再发。

映射的逻辑是用 Node.js 实现的,会在后文中详细讲解。注意,必须要有列号,才能完成代码还原。

二、行为搜集

将行为分成:用户行为、浏览器行为、控制台打印行为。监控这些主要是为了在排查错误时,能还原用户当时的各个动作,从而能更好的找出问题出错的原因。

1)用户行为

目前试验阶段,就监听了点击事件,并且只会对 button 和 a 元素上注册的点击事件做监控。

/**
 * 全局监听事件
 */
var eventHandle = function (eventType, detect) {
  return function (e) {
    if (!detect(e)) {
      return;
    }
    handleAction(ACTION_EVENT, {
      type: eventType,
      desc: e.target.outerHTML
    });
  };
};
// 监听点击事件
window.addEventListener(
  "click",
  eventHandle("click", function (e) {
    var nodeName = e.target.nodeName.toLowerCase();
    // 白名单
    if (nodeName !== "a" && nodeName !== "button") {
      return false;
    }
    // 过滤 a 元素
    if (nodeName === "a") {
      var href = e.target.getAttribute("href");
      if (
        !href ||
        href !== "#" ||
        href.toLowerCase() !== "javascript:void(0)"
      ) {
        return false;
      }
    }
    return true;
  }),
  false
);

2)浏览器行为

监控异步通信,重写 XMLHttpRequest 对象,并通过 Navigator.connection 读取当前的网络环境,例如4G、3G等。

其实还想获取当前用户环境的网速,不过还没有较准确的获取方式,因此并没有添加进来。

var _XMLHttpRequest = window.XMLHttpRequest;   //保存原生的XMLHttpRequest
//覆盖XMLHttpRequest
window.XMLHttpRequest = function (flags) {
  var req;
  req = new _XMLHttpRequest(flags);        //调用原生的XMLHttpRequest
  monitorXHR(req);     //埋入我们的“间谍”
  return req;
};
var monitorXHR = function (req) {
  req.ajax = {};
  req.addEventListener(
    "readystatechange",
    function () {
      if (this.readyState == 4) {
        var end = shin.now();          //结束时间
        req.ajax.status = req.status;     //状态码
        if ((req.status >= 200 && req.status < 300) || req.status == 304) {
          //请求成功
          req.ajax.endBytes = _kb(req.responseText.length * 2) + "KB";   //KB
        } else {
          //请求失败
          req.ajax.endBytes = 0;
        }
        req.ajax.interval = _rounded(end - start, 2) + "ms";   //单位毫秒
        req.ajax.network = shin.network();
        //只记录300个字符以内的响应
        req.responseText.length <= 300 &&
          (req.ajax.response = req.responseText);
        handleAction(ACTION_AJAX, req.ajax);
      }
    },
    false
  );

  // “间谍”又对open方法埋入了间谍
  var _open = req.open;
  req.open = function (type, url, async) {
    req.ajax.type = type;    //埋点
    req.ajax.url = url;     //埋点
    return _open.apply(req, arguments);
  };

  var _send = req.send;
  var start;     //请求开始时间
  req.send = function (data) {
    start = shin.now();      //埋点
    if (data) {
      req.ajax.startBytes = _kb(JSON.stringify(data).length * 2) + "KB";
      req.ajax.data = data;   //传递的参数
    }
    return _send.apply(req, arguments);
  };
};
/**
 * 计算KB值
 */
function _kb(bytes) {
  return _rounded(bytes / 1024, 2);   //四舍五入2位小数
}
/**
 * 四舍五入
 */
function _rounded(number, decimal) {
  return parseFloat(number.toFixed(decimal));
}
/**
 * 网络状态
 */
shin.network = function () {
  var connection =
    window.navigator.connection ||
    window.navigator.mozConnection ||
    window.navigator.webkitConnection;
  var effectiveType = connection && connection.effectiveType;
  if (effectiveType) {
    return { bandwidth: 0, type: effectiveType.toUpperCase() };
  }
  var types = "Unknown Ethernet WIFI 2G 3G 4G".split(" ");
  var info = { bandwidth: 0, type: "" };
  if (connection && connection.type) {
    info.type = types[connection.type];
  }
  return info;
};

在所有的日志中,通信占的比例是最高的,大概在 90% 以上。

浏览器的行为还包括跳转,当前非常流行 SPA,所以在记录跳转地址时,只需监听 onpopstate 事件即可,其中上一页地址也会被记录。

/**
 * 全局监听跳转
 */
var _onPopState = window.onpopstate;
window.onpopstate = function (args) {
  var href = location.href;
  handleAction(ACTION_REDIRECT, {
    refer: shin.refer,
    current: href
  });
  shin.refer = href;
  _onPopState && _onPopState.apply(this, args);
};

3)控制台打印行为

其实就是重写 console 中的方法,目前只对 log() 做了处理。在实际使用中发现了两个问题。

第一个是在项目调试阶段,将数据打印在控制台时,显示的文件和行数都是 SDK 的名称和位置,无法得知真正的位置,很是别扭。

并且在 SDK 的某些位置调用 console.log() 会形成死循环。后面就加了个 isDebug 开关,在调试时就关闭监控,省心。

function injectConsole(isDebug) {
  !isDebug &&
    ["log"].forEach(function (level) {
      var _oldConsole = console[level];
      console[level] = function () {
        var params = [].slice.call(arguments);   // 参数转换成数组
        _oldConsole.apply(this, params);       // 执行原先的 console 方法
        var seen = [];
        handleAction(ACTION_PRINT, {
          level: level,
          // 避免循环引用
          desc: JSON.stringify(params, function (key, value) {
            if (typeof value === "object" && value !== null) {
              if (seen.indexOf(value) >= 0) {
                return;
              }
              seen.push(value);
            }
            return value;
          })
        });
      };
    });
}

第二个就是某些要打印的变量包含循环引用,这样在调用 JSON.stringify() 时就会报错。

三、其他

1)环境信息

通过解析请求中的 UA 信息,可以得到操作系统、浏览器名称版本、CPU等信息。

{
  "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
  "browser": {
    "name": "Chrome",
    "version": "89.0.4389.82",
    "major": "89"
  },
  "engine": {
    "name": "Blink",
    "version": "89.0.4389.82"
  },
  "os": {
    "name": "Mac OS",
    "version": "10.14.6"
  },
  "device": {},
  "cpu": {}
}

图省事,就用了一个开源库,叫做 UAParser.js,在 Node.js 中引用了此库。

2)上报

上报选择了 Gif 的方式,即把参数拼接到一张 Gif 地址后,传送到后台。

/**
 * 组装监控变量
 */
function _paramify(obj) {
  obj.token = shin.param.token;
  obj.subdir = shin.param.subdir;
  obj.identity = getIdentity();
  return encodeURIComponent(JSON.stringify(obj));
}
/**
 * 推送监控信息
 */
shin.send = function (data) {
  var ts = new Date().getTime().toString();
  var img = new Image(0, 0);
  img.src = shin.param.src + "?m=" + _paramify(data) + "&ts=" + ts;
};

用这种方式有几个优势:

  • 兼容性高,所有的浏览器都支持。
  • 不存在跨域问题。
  • 不会携带当前域名中的 cookie。
  • 不会阻塞页面加载。
  • 相比于其他类型的图片格式(BMP、PNG等),能节约更多的网络资源。

不过这种方式也有一个问题,那就是采用 GET 的请求后,浏览器会限制 URL 的长度,也就是不能携带太多的数据。

在之前记录 Ajax 响应数据时就有一个判断,只记录300个字符以内的响应数据,其实就是为了规避此限制而加了这段代码。

3)身份标识

每次进入页面都会生成一个唯一的标识,存储在 sessionStorage 中。在查询日志时,可通过该标识过滤出此用户的上下文日志,消除与他不相干的日志。

function getIdentity() {
  var key = "shin-monitor-identity";
  //页面级的缓存而非全站缓存
  var identity = sessionStorage.getItem(key);
  if (!identity) {
    //生成标识
    identity = Number(
      Math.random().toString().substr(3, 3) + Date.now()
    ).toString(36);
    sessionStorage.setItem(key, identity);
  }
  return identity;
}

 

关于当前Web前端技术的一些感悟和笔记 - 伍华聪 - 博客园

mikel阅读(897)

来源: 关于当前Web前端技术的一些感悟和笔记 – 伍华聪 – 博客园

最近这些年,随着前端应用技术突飞猛进,产生了很多新的前端框架,当然也引入了数不胜数的前端技术概念,前端不在是早期Web Form的拖拉处理方式,也不再是Ajax+HTML那么简单,随着前端技术的发展,前端的JS越来越重要,也越来越复杂,而为了开发的方便,引入了很多可以对JS+CSS进行编译的框架,而在发布的时候按需编译处理,从而增强了整个前端的开发过程,这些前端的技术包括AngularJS、React、Vue等等,这些前端技术应用框架又囊括了很多相关的技术,包括了MVVM(Model-View-ViewModel)、ES6、Babel、dva、umi、less等技术或概念。前端技术越滚越大,范围也越来越广,大有日新月异的感觉。

1、前端技术的自我回顾和展望

记得在上大学时候,开始玩asp的年代,前端和后端糅合一起的困境;也曾记得WebForm开发的乐趣和无奈,快捷但是很丑很笨重;而现在还在继续做着Ajax + HTML的这种前端的处理,痛并快乐着。技术总是一步步的推进则,但是眼光一旦聚焦在某个技术范畴,日月如梭,抬头间很快就会发现世界又多了新的前端技术,从开始的犹豫和不确信的停留这段时间后,发现整个前端的世界也已经渐成格局,包括Angular、React、Vue等技术应用已经日趋成熟,而且拥有着庞大的拥趸群体,也有着丰富的资源可供学习和了解。

下面是Angular、React、Vue几个技术框架的一些介绍。

AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVC(Model–view–controller)、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。Angular开发在全球开发人员中广泛流行,并被谷歌,福布斯,WhatsApp,Instagram,healthcare.gov和许多财富500强公司等大型组织使用。

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。 由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。

Vue.js是讨论最多且发展最快的JavaScript框架之一。它由前谷歌员工Evan You创建,他在担任Google员工时曾在Angular工作过。您可以认为它是成功的,因为它能够使用HTML,CSS和JavaScript构建有吸引力的UI。

这些技术各有优点,很难片面的说明谁优谁劣,它们都各自有自己的生存土壤和大批的拥趸,而我开始选型做前端技术更新的时候,主要看中的是阿里巴巴的Ant-Design开发框架,这个它是使用了React的技术框架,因此也就自然而然的研究学习起React和Ant-Design来,虽然之前对前端的一些技术有所涉猎,但是真正等你想要进入Ant-Design的开发大门的时候,还是感觉自己像进入了一个前端技术的大观园,一个个新概念接踵而来,一种种代码的写法迎面冲击,教程看了几遍还是一头雾水,真的开始怀疑人生了,不过学习新技术还是需要很多平静的心态,调整好,一步一个脚印相信还是有所斩获的,偶尔看到阮一峰的大牛介绍在学习研究React的时候,也曾花了几个月的时候,虽然他的高度难以看齐,但是学习的韧劲和毅力,是值得我们学习的。学习新的东西,从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目(摘自阮一峰笔记)。

 

2、React的技术学习

接触一些新的东西,就必然需要投入精力来学习掌握。对于学习Ant-Desin,虽然这个框架本身提供了很多教程介绍,但是我们一些技术点,还是需要更细节的学习,首推还是阮一峰的技术日志吧。

1、ECMAScript 6 入门

2、React 入门实例教程

3、Redux 入门教程(一):基本用法

4、Redux 入门教程(二):中间件与异步操作

5、Redux 入门教程(三):React-Redux 的用法

6、Redux 文档基础教程

7、DvaJS快速上手

 

下面有些内容在学习的时候,掌握的不是很好,摘录并作为一个回顾吧。

模块的 Import 和 Export

import 用于引入模块,export 用于导出模块。

复制代码
// 引入全部
import dva from 'dva';

// 引入部分
import { connect } from 'dva';
import { Link, Route } from 'dva/router';

// 引入全部并作为 github 对象
import * as github from './services/github';

// 导出默认
export default App;
// 部分导出,需 import { App } from './file'; 引入
export class App extend Component {};
复制代码

析构赋值

析构赋值让我们从 Object 或 Array 里取部分数据存为变量。

复制代码
// 对象
const user = { name: 'guanguan', age: 2 };
const { name, age } = user;
console.log(`${name} : ${age}`);  // guanguan : 2

// 数组
const arr = [1, 2];
const [foo, bar] = arr;
console.log(foo);  // 1
复制代码

我们也可以析构传入的函数参数。

复制代码
const add = (state, { payload }) => {
  return state.concat(payload);
};

//析构时还可以配 alias,让代码更具有语义
const add = (state, { payload: todo }) => {
  return state.concat(todo);
};
复制代码

 

对象展开运算符(Object Spread Operator)

复制代码
//可用于组装数组。
const todos = ['Learn dva'];
[...todos, 'Learn antd'];  // ['Learn dva', 'Learn antd']

//也可用于获取数组的部分项。
const arr = ['a', 'b', 'c'];
const [first, ...rest] = arr;
rest;  // ['b', 'c']

// With ignore
const [first, , ...rest] = arr;
rest;  // ['c']

//还可收集函数参数为数组。
function directions(first, ...rest) {
  console.log(rest);
}
directions('a', 'b', 'c');  // ['b', 'c'];


//代替 apply。
function foo(x, y, z) {}
const args = [1,2,3];

// 下面两句效果相同
foo.apply(null, args);
foo(...args);


//对于 Object 而言,用于组合成新的 Object 
const foo = {
  a: 1,
  b: 2,
};
const bar = {
  b: 3,
  c: 2,
};
const d = 4;

const ret = { ...foo, ...bar, d };  // { a:1, b:3, c:2, d:4 }
复制代码

propTypes

JavaScript 是弱类型语言,所以请尽量声明 propTypes 对 props 进行校验,以减少不必要的问题。

复制代码
function App(props) {
  return <div>{props.name}</div>;
}
App.propTypes = {
  name: React.PropTypes.string.isRequired,
};
复制代码

内置的 prop type 有:

  • PropTypes.array
  • PropTypes.bool
  • PropTypes.func
  • PropTypes.number
  • PropTypes.object
  • PropTypes.string

 

DVA数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。

 

Reducer和effects

reducer 是一个函数,接受 state 和 action,返回老的或新的 state 。即:(state, action) => state

复制代码
app.model({
  namespace: 'todos',
  state: [],
  reducers: {
    add(state, { payload: todo }) {
      return state.concat(todo);
    },
    remove(state, { payload: id }) {
      return state.filter(todo => todo.id !== id);
    },
    update(state, { payload: updatedTodo }) {
      return state.map(todo => {
        if (todo.id === updatedTodo.id) {
          return { ...todo, ...updatedTodo };
        } else {
          return todo;
        }
      });
    },
  },
};
复制代码

建议最多一层嵌套,以保持 state 的扁平化,深层嵌套会让 reducer 很难写和难以维护。

复制代码
app.model({
  namespace: 'app',
  state: {
    todos: [],
    loading: false,
  },
  reducers: {
    add(state, { payload: todo }) {
      const todos = state.todos.concat(todo);
      return { ...state, todos };
    },
  },
});
复制代码

effects示例

复制代码
app.model({
  namespace: 'todos',
  effects: {
    *addRemote({ payload: todo }, { put, call }) {
      yield call(addTodo, todo);
      yield put({ type: 'add', payload: todo });
    },
  },
});
复制代码

put用于触发 action,call用于调用异步逻辑,支持 promise。

 

异步请求

异步请求基于 whatwg-fetch,API 详见:https://github.com/github/fetch

GET 和 POST

复制代码
import request from '../util/request';

// GET
request('/api/todos');

// POST
request('/api/todos', {
  method: 'POST',
  body: JSON.stringify({ a: 1 }),
});
复制代码

统一错误处理

假如约定后台返回以下格式时,做统一的错误处理。

{
  status: 'error',
  message: '',
}

编辑 utils/request.js,加入以下中间件:

复制代码
function parseErrorMessage({ data }) {
  const { status, message } = data;
  if (status === 'error') {
    throw new Error(message);
  }
  return { data };
}
复制代码

然后,这类错误就会走到 onError hook 里。

 

Subscription

subscriptions 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。格式为 ({ dispatch, history }) => unsubscribe 。

异步数据初始化

比如:当用户进入 /users 页面时,触发 action users/fetch 加载用户数据。

复制代码
app.model({
  subscriptions: {
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        if (pathname === '/users') {
          dispatch({
            type: 'users/fetch',
          });
        }
      });
    },
  },
});
复制代码
react dva 的 connect 与 @connect
connect的作用是将组件和models结合在一起。将models中的state绑定到组件的props中。并提供一些额外的功能,譬如dispatch

connect 的使用

connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。

connect 方法传入的第一个参数是 mapStateToProps 函数,该函数需要返回一个对象,用于建立 State 到 Props 的映射关系。

简而言之,connect接收一个函数,返回一个函数。

第一个函数会注入全部的models,你需要返回一个新的对象,挑选该组件所需要的models。

复制代码
export default connect(({ user, login, global = {}, loading }) => ({
    currentUser: user.currentUser,
    collapsed: global.collapsed,
    fetchingNotices: loading.effects['global/fetchNotices'],
    notices: global.notices
}))(BasicLayout);

// 简化版
export default connect( 
  ({ user, login, global = {}, loading }) => {
        return {
          currentUser: user.currentUser,
          collapsed: global.collapsed,
          fetchingNotices: loading.effects['global/fetchNotices'],
          notices: global.notices
        }
  }
)(BasicLayout);
复制代码

@connect的使用

其实只是connect的装饰器、语法糖罢了。

复制代码
// 将 model 和 component 串联起来
export default connect(({ user, login, global = {}, loading }) => ({
    currentUser: user.currentUser,
    collapsed: global.collapsed,
    fetchingNotices: loading.effects['global/fetchNotices'],
    notices: global.notices,
    menuData: login.menuData,         
    redirectData: login.redirectData, 
}))(BasicLayout);
复制代码
复制代码
// 改为这样(export 的不再是connect,而是class组件本身。),也是可以执行的,但要注意@connect必须放在export default class前面:
// 将 model 和 component 串联起来
@connect(({ user, login, global = {}, loading }) => ({
  currentUser: user.currentUser,
  collapsed: global.collapsed,
  fetchingNotices: loading.effects['global/fetchNotices'],
  notices: global.notices,
  menuData: login.menuData,        
  redirectData: login.redirectData, 
}))

export default class BasicLayout extends React.PureComponent { 
   // ...
}
复制代码
export default connect(从 model 的 state 中获取数据)(要将数据绑定到哪个组件) 

以上部分内容摘自 https://blog.csdn.net/zhangrui_web/article/details/79651812

 

2、Ant-Design的框架

这款基于React开发的UI框架,界面非常简洁美观,是阿里巴巴旗下蚂蚁金融服务集团(旗下拥有支付宝、余额宝等产品)所设计的一个前端UI组件库。目前支持了React, 并且有一个对Vue支持的测试版本。

学习和使用Ant-Design,我们可以使用VSCode来对项目代码进行维护和编辑,这样可以在Mac和Window环境同样的开发体验和操作模式,非常方便。

 

如果需要掌握Ant-Design框架,我们需要了解model,namespace,connect,dispatch,action,reducer ,effect这些概念。

DVA 的 model 对象有几个基本的属性介绍。

  1. namespace:model 的命名空间,只能用字符串。一个大型应用可能包含多个 model,通过namespace区分。
  1. state:当前 model 状态的初始值,表示当前状态。
  1. reducers:用于处理同步操作,可以修改 state,由 action 触发。reducer 是一个纯函数,它接受当前的 state 及一个 action 对象。action 对象里面可以包含数据体(payload)作为入参,需要返回一个新的 state。
  1. effects:用于处理异步操作(例如:与服务端交互)和业务逻辑,也是由 action 触发。但是,它不可以修改 state,要通过触发 action 调用 reducer 实现对 state 的间接操作。
  1. action:action 就是一个普通 JavaScript 对象,是 reducers 及 effects 的触发器,形如{ type: 'add', payload: todo },通过 type 属性可以匹配到具体某个 reducer 或者 effect,payload 属性则是数据体,用于传送给 reducer 或 effect。

整体的数据流向见下图:

 

在Reducer里面,不要修改传入的 state。 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。

复制代码
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}
复制代码

或者使用使用对象展开运算符(Object Spread Operator)来处理,从而使用 { ...state, ...newState } 达到相同的目的。

复制代码
  reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
复制代码

在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state

每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

下面两种合成 reducer 方法完全等价:

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})
复制代码
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}
复制代码

dva封装了redux,减少很多重复代码比如action reducers 常量等,dva所有的redux操作是放在models目录下,通过namespace作为key,标识不同的模块state,可以给state设置初始数据。

reducers跟传统的react-redux写法一致,所有的操作放在reducers对象内

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作,Effects 的最终流向是通过 Reducers 改变 State

其中上面的effects里面,call, put其实是saga的写法,dva集成了saga,可以参考上图中的saga内容

 

DVA 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,DVA 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

DVA 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。

在Ant-Design的Pages/.umi目录里面,有一个initDva.js文件,就是用来统一批量处理 DVA 的引入的,如下所示。

在有 DVA 之前,我们通常会创建 sagas/products.jsreducers/products.js 和 actions/products.js,然后在这些文件之间来回切换。

有了 DVA 后,它最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起,这样我们在书写代码的时候,把它主要内容,和加载分离出来。如果建立的Model比较多,每次开始的时候需要加入这一句好像也是挺麻烦的,如果可以自动把这个model批量加入,应该会更好吧,不过不知道是基于什么考量。

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架/混合式开发框架Web开发框架Bootstrap开发框架微信门户开发框架的研究及应用
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com