Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 11848 articles
Browse latest View live

不要开启tcp_tw_recycle

$
0
0

TL;DR:

不要开启 net/ipv4/tcp_tw_recycle

问题描述

我们把Greenplum搬到了kubernetes上面。Greenplum主控分为master和standby两个容器,分别跑在2个物理机器上,容器网络为NAT(flannel);为了方便用户访问,我们为每个租户分配了一个virtual ip(物理机器网段),该地址由keepalived管理,具体实现上我们使用了 kube-keepalived-vip,它也以容器的方式跑在kubernetes上,宿主机网络(因为要向宿主机下发virtual ip),对keepalived本身做了封装:通过configmap传递virtual ip和real server(即master容器的ip地址,rmt50-vip-svc的endpoint),并生成keepalived的配置文件,然后启动keepalived。keepalived会向内核下发lvs规则,将到virtual ip + 5432(192.168.128.5 5432)的报文走NAT转发给master容器(10.244.3.27 5432)。

topo

configmap:

apiVersion:v1data:192.168.128.5:default/rmt50-vip-svckind:ConfigMap

具体keepalived.conf:

vrrp_instance vips {
  state BACKUP
  interface eno16780032
  virtual_router_id 28
  priority 100
  nopreempt
  advert_int 1

  track_interface {
    eno16780032
  }

  virtual_ipaddress { 
    192.168.128.5
  }
}

# Service: default/rmt50-vip
virtual_server 192.168.128.5 5432 {
  delay_loop 5
  lvs_sched wlc
  lvs_method NAT
  persistence_timeout 1800
  protocol TCP
  
  real_server 10.244.3.27 5432 {
    weight 1
    TCP_CHECK {
      connect_port 5432
      connect_timeout 3
      retry 1
      delay_before_retry 3
    }
  }
}

该租户各个容器的地址如下。其中rmt50-master为master容器,rmt50-standby是standby容器,rmt50-vip为vip容器。

kubectl get pods -o wide|grep rmt50
rmt50-master              1/1       Running    0          2d        10.244.3.27       s2.adb.g1.com
rmt50-standby             1/1       Running    0          2d        10.244.1.250      m2.adb.g1.com
rmt50-vip                 1/1       Running    5          2d        192.168.128.149   s2.adb.g1.com

但实际使用时,发现psql连接virtual ip(192.168.128.5)有一定概率会失败,服务器反馈端口不可达;查看kube-keepalived-vip容器(下面简称vip容器)的日志,发现是TCP_CHECK检测real_server超时后,将real_server从内核lvs表项中删除了(通过ipvsadm -l查看)。

一开始我以为是网络抖动,所以把connection_timeout, retry都调大了一点(retry 1确实有点太严苛了),之后的确有所改善,但问题并没有根除,如果从另一物理机器(m1)和vip容器上同时去频繁psql连接virtual ip,基本上几分钟就可以复现出来。

定位过程

怀疑点1:网络

出现问题时,docker exec进入vip容器,发现ping master容器正常,说明从vip容器到master容器的网络是正常的。由于vip容器和master容器都在同一个物理机器上(s1),只是vip容器在宿主机网络上,而master容器在flannel网络内;由于宿主机网络有所有的路由,因此从vip容器出来的报文,直接走cni0转发即可,源地址使用cni0的地址,一般来说不太会出问题。

怀疑点2:postgre进程故障

在master容器上tcpdump抓包,可以看到vip 容器发过来的syn报文,但是master容器并没有应答,导致syn报文一直重传。如果postgre进程跑飞了会不会造成这个现象呢?在出问题的时候,我 strace -p pid了下postgre进程,看到一直在select,说明内核没有上报新socket事件。

其实,根本就不应该怀疑postgre进程。从抓包看到只有收到syn没有应答syn+ack,就可以肯定跟用户态的postgre进程无关了:tcp三次握手完全是内核完成的,只有3次握手结束,内核才会给用户态进程上报事件;用户态最多就是不响应该事件,但不会造成不应答syn+ack。

怀疑点3:内核丢包

只有这种可能了。linux内核调试起来比较麻烦,不过可以通过proc来看一些统计信息。

cat /proc/net/netstat 
TcpExt: SyncookiesSent SyncookiesRecv SyncookiesFailed EmbryonicRsts PruneCalled RcvPruned OfoPruned OutOfWindowIcmps LockDroppedIcmps ArpFilter TW TWRecycled TWKilled PAWSPassive PAWSActive PAWSEstab DelayedACKs DelayedACKLocked DelayedACKLost ListenOverflows ListenDrops TCPPrequeued TCPDirectCopyFromBacklog TCPDirectCopyFromPrequeue TCPPrequeueDropped TCPHPHits TCPHPHitsToUser TCPPureAcks TCPHPAcks TCPRenoRecovery TCPSackRecovery TCPSACKReneging TCPFACKReorder TCPSACKReorder TCPRenoReorder TCPTSReorder TCPFullUndo TCPPartialUndo TCPDSACKUndo TCPLossUndo TCPLostRetransmit TCPRenoFailures TCPSackFailures TCPLossFailures TCPFastRetrans TCPForwardRetrans TCPSlowStartRetrans TCPTimeouts TCPLossProbes TCPLossProbeRecovery TCPRenoRecoveryFail TCPSackRecoveryFail TCPSchedulerFailed TCPRcvCollapsed TCPDSACKOldSent TCPDSACKOfoSent TCPDSACKRecv TCPDSACKOfoRecv TCPAbortOnData TCPAbortOnClose TCPAbortOnMemory TCPAbortOnTimeout TCPAbortOnLinger TCPAbortFailed TCPMemoryPressures TCPSACKDiscard TCPDSACKIgnoredOld TCPDSACKIgnoredNoUndo TCPSpuriousRTOs TCPMD5NotFound TCPMD5Unexpected TCPSackShifted TCPSackMerged TCPSackShiftFallback TCPBacklogDrop TCPMinTTLDrop TCPDeferAcceptDrop IPReversePathFilter TCPTimeWaitOverflow TCPReqQFullDoCookies TCPReqQFullDrop TCPRetransFail TCPRcvCoalesce TCPOFOQueue TCPOFODrop TCPOFOMerge TCPChallengeACK TCPSYNChallenge TCPFastOpenActive TCPFastOpenActiveFail TCPFastOpenPassive TCPFastOpenPassiveFail TCPFastOpenListenOverflow TCPFastOpenCookieReqd TCPSpuriousRtxHostQueues BusyPollRxPackets TCPAutoCorking TCPFromZeroWindowAdv TCPToZeroWindowAdv TCPWantZeroWindowAdv TCPSynRetrans TCPOrigDataSent TCPHystartTrainDetect TCPHystartTrainCwnd TCPHystartDelayDetect TCPHystartDelayCwnd TCPACKSkippedSynRecv TCPACKSkippedPAWS TCPACKSkippedSeq TCPACKSkippedFinWait2 TCPACKSkippedTimeWait TCPACKSkippedChallenge
TcpExt: 0 0 118 0 0 0 0 0 1 0 94979 0 960494 0 0 906 343087 1119 5435 0 0 432477 9530 21215128 0 7057137 7 2736264 2582522 0 4 0 1 2 0 0 0 0 6 317 0 0 5 0 92 0 23 1992 1464 713 0 0 4 0 5450 62 1557 0 293509 80 0 12 0 0 0 0 0 782 49 0 0 0 0 171 0 0 0 0 0 0 0 2 1743889 11075 0 69 7 3 0 0 0 0 0 0 174 0 303158 542 542 3641 3021 21292349 27 749 0 0 0 0 0 0 0 0
IpExt: InNoRoutes InTruncatedPkts InMcastPkts OutMcastPkts InBcastPkts OutBcastPkts InOctets OutOctets InMcastOctets OutMcastOctets InBcastOctets OutBcastOctets InCsumErrors InNoECTPkts InECT1Pkts InECT0Pkts InCEPkts
IpExt: 3 0 4802567 1188058 6661 0 57132122839 92642737581 189351200 38019672 2189009 0 0 85932763 0 0 0

非常友好的统计信息!

回头还是用go写个小工具format下netstat,这个真的太难看了。我用atom把netstat竖排了下,发现有个ListenDrops计数在出现问题的时候会有变大。呵呵呵,坏人抓到了。

这里可以用我写的 netproc来观察net计数的变化,还是比较友好的!

来看内核的这个计数是干啥的。

ListenDrops计数对应内核的 LINUX_MIB_LISTENDROPS,走读下代码,可以看到有很多情况该计数会增加,为了区分不同情况,内核会在增加 LINUX_MIB_LISTENDROPS的同时增加其他的计数。不慌,我们再看下netstat的信息,有没有发现PAWSPassive的计数跟ListenDrops是一致的?是不是巧合呢?没关系,我们再重复一把,看看netstat的变化。

果然还是一致的。

两次netstat的信息:

PAWSPassive 3565   3858
ListenDrops 3565   3858

PAWSPassive对应内核的 LINUX_MIB_PAWSPASSIVEREJECTED,它只有一种情况下会出现:

tcp_ipv4.c/tcp_v4_conn_request:

inttcp_v4_conn_request(structsock*sk,structsk_buff*skb)/* VJ's idea. We save last timestamp seen
		 * from the destination in peer table, when entering
		 * state TIME-WAIT, and check against it before
		 * accepting new connection request.
		 *
		 * If "isn" is not zero, this request hit alive
		 * timewait bucket, so that all the necessary checks
		 * are made in the function processing timewait state.
		 */if(tmp_opt.saw_tstamp&&tcp_death_row.sysctl_tw_recycle&&(dst=inet_csk_route_req(sk,&fl4,req))!=NULL&&fl4.daddr==saddr){if(!tcp_peer_is_proven(req,dst,true)){NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_PAWSPASSIVEREJECTED);gotodrop_and_release;}}drop_and_release:dst_release(dst);drop_and_free:reqsk_free(req);drop:NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_LISTENDROPS);return0;

net/ipv4/tcp_metrics.c:

booltcp_peer_is_proven(structrequest_sock*req,structdst_entry*dst,boolpaws_check){structtcp_metrics_block*tm;boolret;if(!dst)returnfalse;rcu_read_lock();tm=__tcp_get_metrics_req(req,dst);if(paws_check){if(tm&&(u32)get_seconds()-tm->tcpm_ts_stamp<TCP_PAWS_MSL&&(s32)(tm->tcpm_ts-req->ts_recent)>TCP_PAWS_WINDOW)ret=false;elseret=true;}

内核版本为3.10。

简单来说,当开启了tcp_tw_recycle时,kernel会记录每个peer的最后一个报文的时戳,如果记录的该时戳仍然有效(距离当前时间小于TCP_PAWS_MSL),并且新收到的syn报文的时戳,比kernel记录的该peer的时戳还要小(换句话说,时光倒流了),那么就认为新收到的syn报文是有问题的(比如是某个在网络上兜兜转转了很久才到目的地址的syn),从而drop之。

所以我们看到的现象就是,内核收到了新的syn报文,但只是默默drop了(没什么好处理方法,回RST可能误伤),所以造成了psql连接超时。

解决方法

只要将net.ipv4.tcp_tw_recycle恢复为默认值0即可。

深入理解

TIME_WAIT是干啥的

先祭出tcp状态机迁移图。做协议栈的都要能默写啊!

tcp_state

只有主动关闭连接的一方,才会转移到TIME_WAIT。

TIME_WAIT的主要目的有2个:

避免误收延迟到达的报文

如下图,由于TIME_WAIT的时间被缩短了,造成新建的连接收到了之前延迟到达的报文(5元组是匹配的)。

tcp_state

保证对端已经关闭了连接

如下图,由于TIME_WAIT的时间被缩短了,对端还处于LAST_ACK状态,本端发送的syn报文被直接RST掉了。

tcp_state

为什么Greenplum会开启tcp_tw_recycle

为什么内核会开启 net.ipv4.tcp_tw_recycle=1呢?网络上有很多资料建议繁忙的服务器开启这个sysctl,Greenplum也在其官网的 资料Linux System Settings里提到,linux的/etc/sysctl.conf中应设置 net.ipv4.tcp_tw_recycle=1

开启tcp_tw_recycle的目的是为了减少TIME_WAIT状态的socket连接,从而减少内存、cpu的使用,因为TIME_WAIT状态的socket会快速释放;也可以提高并发连接的规格,因为客户端可以使用的端口号更多了。对比下没有开启recycle的情况,若服务端先关闭连接,socket会停留在TIME_WAIT状态的时间是1个TCP_PAWS_MSL(在linux上是1分钟),在此时间内,客户端不能再使用刚刚用过的源端口号,否则服务端会直接RST之。

看上去很美好。

为什么不要开启tcp_tw_recycle

但人算不如天算,当网络中存在NAT的情况下,开启tcp_tw_recycle会引起上述syn报文被丢弃的问题。我们来看下为什么。

从前面问题定位的过程可以发现,tcp_tw_recycle能够运转,其基础是tcp报文中需要带TIMESTAMP时戳选项。要了解tcp_tw_recycle,必然要先了解下tcp时戳选项。 TCP timestamp这篇文章中详细的讲解了时戳,建议先跳转过去看看。

简单来说,TCP协议中有一个很重要的概念:RTO(Retransmission TimeOut),重传超时时间,RTO是根据RTT(Round Trip Time)来动态调整的。但如何测量RTT呢?

一个办法是计算报文发送时间和对端ack确认的时间差,作为RTT。但由于报文重传、SACK等原因,这样计算出来的RTO可能会偏大,因此一般会选择没有重传的报文来计算。

另一个办法就是使用TIMESTAMP选项。

  1. 发送方在发送数据时,将一个timestamp(表示发送时间)放在包里面
  2. 接收方在收到数据包后,在对应的ACK包中将收到的timestamp返回给发送方(echo back)
  3. 发送发收到ACK包后,用当前时刻now - ACK包中的timestamp就能得到准确的RTT

时戳的值,在linux上是这样定义的:

include/net/tcp.h

/* TCP timestamps are only 32-bits, this causes a slight
 * complication on 64-bit systems since we store a snapshot
 * of jiffies in the buffer control blocks below.  We decided
 * to use only the low 32-bits of jiffies and hide the ugly
 * casts with the following macro.
 */#define tcp_time_stamp		((__u32)(jiffies))

jiffies即系统从开机到现在的时钟中断次数。

回到tcp_tw_recycle上来。我们来看下tcp_peer_is_proven这个函数中是怎么判断是否应该丢弃该syn报文的。

if(tm&&(u32)get_seconds()-tm->tcpm_ts_stamp<TCP_PAWS_MSL&&(s32)(tm->tcpm_ts-req->ts_recent)>TCP_PAWS_WINDOW)//hereret=false;elseret=true;

只要新包的时戳比上次看到的时戳小,就判断报文有问题。可见,同一个peer的时戳,必须要线性增长,否则判断会出错。显然,一般情况下对于同一个客户端,”时戳线性增长”这个前提是满足的,但如果客户端在NAT之后呢?

我们在家里访问外部服务器的时候,家里可能有多台终端,其地址可能是192.168.1.100, 192.168.1.101,但在从家庭路由器出去的时候,会做一次SNAT,将报文的源地址替换为运营商给我们分配的公网地址。对于服务器来说,只能看到该公网地址。但由于2台终端开机的时间不一样,其报文中的TIMESTAMP选项值也不一样。因此,如果192.168.1.100的开机时间比192.168.1.101早,可能会出现192.168.1.100的连接关闭以后,192.168.1.101无法立即建立连接,因为后收到的syn报文的TIME_STAMP的值更小。在服务端来看,时间不可能倒流,那么新来的syn报文可能是个迟到的家伙,因此必然会被drop,192.168.1.101只能等一会才能建立连接(TCP_PAWS_MSL之后)。

如果192.168.1.100在频繁的连接建立、断开,192.168.1.101可能很久都无法连接的上。

特殊国情

但我们的环境中,并不符合上述客户端在NAT里面的情况,而是反过来:master容器在NAT里面。

客户端访问virtual ip时,内核会做一次DNAT,将报文目的地址virtual ip转为master的ip,源地址不变。如果之后报文直接走cni0进入master容器,其实不会出现上面的问题:报文源地址是不同的。但由于kubernetes的缘故,报文在POSTROUTING阶段,还是会做一次SNAT(此处应该给生哥掌声):

Chain POSTROUTING (policy ACCEPT 6 packets, 360 bytes)
 pkts bytes target     prot opt in     out     source               destination
6351K  563M KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0
2601K  177M RETURN     all  --  *      *       10.244.0.0/16        10.244.0.0/16
 102K 6466K MASQUERADE  all  --  *      *       10.244.0.0/16       !224.0.0.0/4
1248K   86M MASQUERADE  all  --  *      *      !10.244.0.0/16        10.244.0.0/16

即:若报文的源地址和目的地址一个是10.244.0.0/16网段,一个不是,则走MASQUERADE,即做一次SNAT。MASQUERADE表示源地址不静态指定,而是动态选择。

因此,从m1上发出的psql请求,在经过lvs的一次DNAT和POSTROUTING阶段的一次SNAT之后,报文的源地址和目的地址,从192.168.128.158->192.168.128.5,变为了10.244.3.1->10.244.3.27;而从vip容器发出的psql请求,其源地址和目的地址就是10.244.3.1->10.244.3.27。

悲剧就发生了。

由于vip容器所在的机器启动时间要晚一点,因此受害者总是它,也就是我们一开始所描述的vip容器去psql连接master容器超时,但从m1上访问总是没问题的现象。

btw:上面的lvs+iptables实现了 FULLNAT的效果,可以不给内核打阿里的补丁。

总结

tcp_tw_recycle这个选项在内核的文档里说明的比较含糊,但是有一句警告:

Enable fast recycling TIME-WAIT sockets. Default value is 0. It should not be changed without advice/request of technical experts.

意思就是:特殊勤务,请勿靠近。

不过man 7 tcp里倒是挺干脆的提示:

Enable fast recycling of TIME_WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation).

ref:



查看历史执行计划_ITPUB博客

$
0
0
如果要查看过去历史真实的执行计划,而不是使用explain plan命令即时解析,需要查看相关视图。

如果当前执行计划仍然保存在library cache,则可以从v$sql_plan中看到。

点击(此处)折叠或打开

  1. SELECT plan_hash_value,
  2.        TO_CHAR(RAWTOHEX(child_address)),
           TO_NUMBER(child_number),
           id,
           LPAD(' ', DEPTH) || operation operation,
           options,
           object_owner,
           object_name,
           optimizer,
           cost,
           access_predicates,
           filter_predicates
      FROM V$SQL_PLAN
      where sql_id = 'abcd'
     ORDER BY 1, 3, 2, 4
黄色文字部分为sql_id

如果执行计划已经不在library cache中了,则需要去DBA_HIST_SQL_PLAN中寻找。

点击(此处)折叠或打开

  1. set linesize 500
  2. set pagesize 500
    col plan_hash_value format 9999999999
    col id format 999999
    col operation format a30
    col options format a15
    col object_owner format a15
    col object_name format a20
    col optimizer format a15
    col cost format 9999999999
    col access_predicates format a15
    col filter_predicates format a15

    SELECT plan_hash_value,
             id,
             LPAD (' ', DEPTH) || operation operation,
             options,
             object_owner,
             object_name,
             optimizer,
             cost,
             access_predicates,
             filter_predicates
       FROM dba_hist_sql_plan
       WHERE sql_id = 'fahv8x6ngrb50'
    ORDER BY plan_hash_value, id;

通常,在AWR中发现的可疑语句可以通过如上方式操作。

基于qiankun框架的微前端实战使用_DJYanggggg的博客-CSDN博客_qiankun框架使用教程

$
0
0

   最近公司要整合目前所有的前端项目,希望放到同一个项目里面进行管理,但是项目使用的技术栈大体不相同,有原生js的,有用jq的也有用Vue的,整合的话要么重构,统一技术栈,不过这是不现实的,成本高,时间长,要么使用iframe,但是iframe也有很多缺点,通信不方便,刷新页面会导致路由丢失,这都是很不好的体验,于是我想起了最近很火的微前端概念,打算用微前端的技术来整合已有项目。

  1. 什么是微前端?

    微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。微前端的核心在于 , 拆完后在 !
     
  2. 为什么使用微前端?

    ① 不同团队间开发同一个应用技术栈不同
    ② 希望每个团队都可以独立开发,独立部署
    ③ 项目中还需要老的应用代码

    我们可以将一个应用划分成若干个子应用,将子应用打包成一个个的lib。当路径切换 时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前端协同开发问题。
     
  3. 如何落地微前端?
     
    2018年Single-SPA诞生了,single-spa是一个用于前端微服务化的JavaScript前端解决方案 (本身没有处理样式隔离,js执行隔离)实现了路由劫持和应用加载。
     
    2019年qiankun基于Single-SPA,提供了更加开箱即用的API(single-spa+sandbox+import-html-entry) 做到了,技术栈无关、并且接入简单(像iframe一样简单)。

     
  4. qiankun框架实操

    这里我们打算建立三个项目进行实操,一个Vue项目充当主应用,另一个Vue和React应用充当子应用,话不多说,直接开干。
    首先我们安装qiankun
    yarn add qiankun 或者 npm i qiankun -S

    安装完qiankun后我们在创建主应用,也是我们的基座

    vue create qiankun-base

    接着创建vue子应用

    vue create qiankun-vue

接着创建react子应用

cnpm install -g create-react-app
create-react-app my-app

创建好后的目录如下,qiankun-js忽略                           

 

 

项目创建好后我们首先进行主应用qiankun-base的配置,进入man.js文件进行配置, 在main.js中加入以下代码,要注意的是,entry这项配置是我们两个子项目的域名和端口,我们必须确保两字子项目运行在这两个端口上面,container就是我们的容器名,就是我们子应用挂载的节点,相当于Vue项目里面的app节点,activeRule就是我们的激活路径,根据路径来显示不同的子应用。

import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'vueApp', // 应用的名字
    entry: '//localhost:8021',// 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    container: '#vue', // 容器名
    activeRule: '/vue',// 激活的路径
  },
  {
    name: 'reactApp',
    entry: '//localhost:8020',
    container: '#react',
    activeRule: '/react',
  },
]);
start();

配置完之后我们去到qiankun-base的app.vue文件进行主应用的页面编写,这里我安装了element-ui来进行页面美化,大家可以安装一下,

npm i element-ui -S

修改app.vue的组件代码如下
 

<template><div><el-menu :router="true" mode="horizontal"><!--基座中可以放自己的路由--><el-menu-item index="/">Home</el-menu-item> <!--引用其他子应用--><el-menu-item index="/vue">vue应用</el-menu-item><el-menu-item index="/react">react应用</el-menu-item></el-menu><router-view ></router-view><div id="vue"></div><div id="react"></div></div></template>

大家可以看到,在elementui的路由导航模式下,菜单子元素的index就是要跳转的路径,这个路径和我们刚刚在main.js编写activeRule是一致,子应用的切换就是根据这里的index进行监听。

接下来我们进行router的配置

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
	mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

我们运行一下qiankun-base

npm run serve

界面应该是这样子的

目前页面除了菜单是没有其他东西的,点击也是没有效果的,因为我们还没配置子应用,现在我们来配置子应用
打开qiankun-vue目录, 在子Vue应用的main.js中加入以下代码

import Vue from 'vue'
import App from './App.vue'
import router from './router'

let instance = null; 

//挂载实例
function render(){ 
    instance = new Vue({ router, render: h => h(App) }).$mount('#app') 
}
//判断当前运行环境是独立运行的还是在父应用里面进行运行,配置全局的公共资源路径
if(window.__POWERED_BY_QIANKUN__){ 
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; 
}
//如果是独立运行window.__POWERED_BY_QIANKUN__=undefined
if(!window.__POWERED_BY_QIANKUN__){
    render()
} 
//最后暴露的三个方法是固定的,加载渲染以及销毁
export async function bootstrap(){} 
export async function mount(props){
    render();
} 
export async function unmount(){
    instance.$destroy();
}

配置完main.js后我们继续配置基础配置模块,我们在子Vue应用的根目录下面新建一个Vue.config.js文件

module.exports = {
    devServer:{
        port:10000,//这里的端口是必须和父应用配置的子应用端口一致
        headers:{
            //因为qiankun内部请求都是fetch来请求资源,所以子应用必须允许跨域
            'Access-Control-Allow-Origin':'*' 
        }
    },
    configureWebpack:{
        output:{
            //资源打包路径
            library:'vueApp',
            libraryTarget:'umd'
        }
    }
}

接下里再进行router的配置

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: '/vue',
  routes
})

export default router

这三步就已经把子应用配置好了,接下来我们再进行子React应用的配置。

重写子React的src目录下的index.js文件,如下

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

function render(){
  ReactDOM.render(
    <React.StrictMode><App /></React.StrictMode>,
    document.getElementById('root')
  );
}
if(!window.__POWERED_BY_QIANKUN__){
  render();
}
export async function bootstrap(){

}
export async function mount() {
  render()
}
export async function unmount(){
  ReactDOM.unmountComponentAtNode( document.getElementById('root'));
}

再进行dev以及打包的配置,根目录下面的config-overrides.js

module.exports = {
    webpack:(config)=>{
        config.output.library = 'reactApp';
        config.output.libraryTarget = 'umd';
        config.output.publicPath = 'http://localhost:20000/';
        return config;
    },
    devServer:(configFunction)=>{
        return function (proxy,allowedHost){
            const config = configFunction(proxy,allowedHost);
            config.headers = {"Access-Control-Allow-Origin":'*'
            }
            return config
        }
    }
}

子React的基础配置和子Vue基本相同,在这里我们就算是配置完了,接下来我们看看结果如何,启动主应用以及两个子应用

上面是我录制的一个gif,可以看到我们的微前端实践已经成功了,点击相应的菜单可以跳转到对应的子应用,并且子应用内的路由是不会受影响的。还有一个需要注意的是,我们的应用路由模式要用history模式,没有的要去router文件里面定义

const router = new VueRouter({ 
    mode: 'history', 
    base: '/vue', 
    routes 
})

那么今天的微前端的分享就到这里了,我可能讲得不太好,希望对大家有用,更加详细的教程和相关问题解答可以上官网

https://qiankun.umijs.org/zh

最后附上例子的源码地址, https://github.com/DJYang666/qiankun.git,喜欢的多多关注哦

微前端qiankun从搭建到部署的实践 - SegmentFault 思否

$
0
0

最近负责的新项目用到了 qiankun,写篇文章分享下实战中遇到的一些问题和思考。

示例代码: https://github.com/fengxianqi/qiankun-example

在线demo: http://qiankun.fengxianqi.com/

单独访问在线子应用:

为什么要用qiankun

项目有个功能需求是需要内嵌公司内部的一个现有工具,该工具是独立部署的且是用 React写的,而我们的项目主要技术选型是 vue,因此需要考虑嵌入页面的方案。主要有两条路:

  • iframe方案
  • qiankun微前端方案

两种方案都能满足我们的需求且是可行的。不得不说, iframe方案虽然普通但很实用且成本也低, iframe方案能覆盖大部分的微前端业务需求,而 qiankun对技术要求更高一些。

技术同学对自身的成长也是有强烈需求的,因此在两者都能满足业务需求时,我们更希望能应用一些较新的技术,折腾一些未知的东西,因此我们决定选用 qiankun

项目架构

后台系统一般都是上下或左右的布局。下图粉红色是基座,只负责头部导航,绿色是挂载的整个子应用,点击头部导航可切换子应用。
image

参考官方的 examples代码,项目根目录下有基座 main和其他子应用 sub-vuesub-react,搭建后的初始目录结构如下:

├── common     //公共模块
├── main       // 基座
├── sub-react  // react子应用
└── sub-vue    // vue子应用

基座是用 vue搭建,子应用有 reactvue

基座配置

基座main采用是的Vue-Cli3搭建的,它只负责导航的渲染和登录态的下发,为子应用提供一个挂载的容器div,基座应该保持简洁(qiankun官方demo甚至直接使用原生html搭建),不应该做涉及业务的操作。

qiankun这个库只需要在基座引入,在 main.js中注册子应用,为了方便管理,我们将子应用的配置都放在: main/src/micro-app.js下。

const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
    container: '#subapp-viewport', // 子应用挂载的div
    props: {
      routerBase: '/sub-vue' // 下发路由给子应用,子应用根据该值去定义qiankun环境下的路由
    }
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
    container: '#subapp-viewport', // 子应用挂载的div
    props: {
      routerBase: '/sub-react'
    }
  }
]

export default microApps

然后在 src/main.js中引入

import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
}).$mount('#app');


registerMicroApps(microApps, {
  beforeLoad: app => {
    console.log('before load app.name====>>>>>', app.name)
  },
  beforeMount: [
    app => {
      console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
    },
  ],
  afterMount: [
    app => {
      console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
    }
  ],
  afterUnmount: [
    app => {
      console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
    },
  ],
});

start();

App.vue中,需要声明 micro-app.js配置的子应用挂载div(注意id一定要一致),以及基座布局相关的,大概这样:

<template><div id="layout-wrapper"><div class="layout-header">头部导航</div><div id="subapp-viewport"></div></div></template>

这样,基座就算配置完成了。项目启动后,子应用将会挂载到 <div id="subapp-viewport"></div>中。

子应用配置

一、vue子应用

用Vue-cli在项目根目录新建一个 sub-vue的子应用,子应用的名称最好与父应用在 src/micro-app.js中配置的名称一致(这样可以直接使用 package.json中的 name作为output)。

  1. 新增 vue.config.js,devServer的端口改为与主应用配置的一致,且加上跨域 headersoutput配置。
// package.json的name需注意与主应用一致
const { name } = require('../package.json')

module.exports = {
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    }
  },
  devServer: {
    port: process.env.VUE_APP_PORT, // 在.env中VUE_APP_PORT=7788,与父应用的配置一致
    headers: {
      'Access-Control-Allow-Origin': '*' // 主应用获取子应用时跨域响应头
    }
  }
}
  1. 新增 src/public-path.js
(function() {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}/`;
      return;
    }
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
})();
  1. src/router/index.js改为只暴露routes, new Router改到 main.js中声明。
  2. 改造 main.js,引入上面的 public-path.js,改写render,添加生命周期函数等,最终如下:
import './public-path' // 注意需要引入public-path
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'
import VueRouter from 'vue-router'

Vue.config.productionTip = false
let instance = null

function render (props = {}) {
  const { container, routerBase } = props
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
    mode: 'history',
    routes
  })
  instance = new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}

if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap () {
  console.log('[vue] vue app bootstraped')
}

export async function mount (props) {
  console.log('[vue] props from main framework', props)

  render(props)
}

export async function unmount () {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
}

至此,基础版本的vue子应用配置好了,如果 routervuex不需用到,可以去掉。

二、react子应用

  1. 通过 npx create-react-app sub-react新建一个react应用。
  2. 新增 .env文件添加 PORT变量,端口号与父应用配置的保持一致。
  3. 为了不 eject所有webpack配置,我们用 react-app-rewired方案复写webpack就可以了。
  • 首先 npm install react-app-rewired --save-dev
  • 新建 sub-react/config-overrides.js
const { name } = require('./package.json');

module.exports = {
  webpack: function override(config, env) {
    // 解决主应用接入后会挂掉的问题:https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter(
      (e) => !e.includes('webpackHotDevClient')
    );
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.open = false;
      config.hot = false;
      config.headers = {'Access-Control-Allow-Origin': '*',
      };
      return config;
    };
  },
};
  1. 新增 src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 改造 index.js,引入 public-path.js,添加生命周期函数等。
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log(props);
  render();
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
  console.log('update props', props);
}

serviceWorker.unregister();

至此,基础版本的react子应用配置好了。

进阶

全局状态管理

qiankun通过 initGlobalState, onGlobalStateChange, setGlobalState实现主应用的全局状态管理,然后默认会通过 props将通信方法传递给子应用。先看下官方的示例用法:

主应用:

// main/src/main.js
import { initGlobalState } from 'qiankun';
// 初始化 state
const initialState = {
  user: {} // 用户信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

子应用:

// 从生命周期 mount 中获取通信方法,props默认会有onGlobalStateChange和setGlobalState两个api
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

这两段代码不难理解,父子应用通过 onGlobalStateChange这个方法进行通信,这其实是一个发布-订阅的设计模式。

ok,官方的示例用法很简单也完全够用,纯JavaScript的语法,不涉及任何的vue或react的东西,开发者可自由定制。

如果我们直接使用官方的这个示例,那么数据会比较松散且调用复杂,所有子应用都得声明 onGlobalStateChange对状态进行监听,再通过 setGlobalState进行更新数据。

因此,我们很有必要 对数据状态做进一步的封装设计。笔者这里主要考虑以下几点:

  • 主应用要保持简洁简单,对子应用来说,主应用下发的数据就是一个很纯粹的 object,以便更好地支持不同框架的子应用,因此主应用不需用到 vuex
  • vue子应用要做到能继承父应用下发的数据,又支持独立运行。

子应用在 mount声明周期可以获取到最新的主应用下发的数据,然后将这份数据注册到一个名为 global的vuex module中,子应用通过global module的action动作进行数据的更新,更新的同时自动同步回父应用。

因此,对子应用来说, 它不用知道自己是一个qiankun子应用还是一个独立应用,它只是有一个名为 global的module,它可通过action更新数据,且不再需要关心是否要同步到父应用(同步的动作会封装在方法内部,调用者不需关心),这也是为后面 支持子应用独立启动开发做准备

  • react子应用同理(笔者react用得不深就不说了)。

image

主应用的状态封装

主应用维护一个 initialState的初始数据,它是一个 object类型,会下发给子应用。

// main/src/store.js

import { initGlobalState } from 'qiankun';
import Vue from 'vue'

//父应用的初始state
// Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
let initialState = Vue.observable({
  user: {},
});

const actions = initGlobalState(initialState);

actions.onGlobalStateChange((newState, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log('main change', JSON.stringify(newState), JSON.stringify(prev));

  for (let key in newState) {
    initialState[key] = newState[key]
  }
});

// 定义一个获取state的方法下发到子应用
actions.getGlobalState = (key) => {
  // 有key,表示取globalState下的某个子级对象
  // 无key,表示取全部
  return key ? initialState[key] : initialState
}

export default actions;

这里有两个注意的地方:

  • Vue.observable是为了让父应用的state变成可响应式,如果不用Vue.observable包一层,它就只是一个纯粹的object,子应用也能获取到,但会失去响应式, 意味着数据改变后,页面不会更新
  • getGlobalState方法,这个是 有争议的,大家在github上有讨论: https://github.com/umijs/qiankun/pull/729

一方面,作者认为 getGlobalState不是必须的, onGlobalStateChange其实已经够用。

另一方面,笔者和其他提pr的同学觉得有必要提供一个 getGlobalState的api,理由是get方法更方便使用,子应用有需求是不需一直监听stateChange事件,它只需要在首次mount时通过getGlobalState初始化一次即可。在这里,笔者先坚持己见让父应用下发一个getGlobalState的方法。

由于官方还不支持getGlobalState,所以需要显示地在注册子应用时通过props去下发该方法:

import store from './store';
const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
  }
]

const apps = microApps.map(item => {
  return {
    ...item,
    container: '#subapp-viewport', // 子应用挂载的div
    props: {
      routerBase: item.activeRule, // 下发基础路由
      getGlobalState: store.getGlobalState // 下发getGlobalState方法
    },
  }
})

export default microApps

vue子应用的状态封装

前面说了,子应用在mount时会将父应用下发的state,注册为一个叫 global的vuex module,为了方便复用我们封装一下:

// sub-vue/src/store/global-register.js

/**
 * 
 * @param {vuex实例} store 
 * @param {qiankun下发的props} props 
 */
function registerGlobalModule(store, props = {}) {
  if (!store || !store.hasModule) {
    return;
  }

  // 获取初始化的state
  const initState = props.getGlobalState && props.getGlobalState() || {
    menu: [],
    user: {}
  };

  // 将父应用的数据存储到子应用中,命名空间固定为global
  if (!store.hasModule('global')) {
    const globalModule = {
      namespaced: true,
      state: initState,
      actions: {
        // 子应用改变state并通知父应用
        setGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
          commit('emitGlobalState', payload);
        },
        // 初始化,只用于mount时同步父应用的数据
        initGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
        },
      },
      mutations: {
        setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload);
        },
        // 通知父应用
        emitGlobalState(state) {
          if (props.setGlobalState) {
            props.setGlobalState(state);
          }
        },
      },
    };
    store.registerModule('global', globalModule);
  } else {
    // 每次mount时,都同步一次父应用数据
    store.dispatch('global/initGlobalState', initState);
  }
};

export default registerGlobalModule;

main.js中添加global-module的使用:

import globalRegister from './store/global-register'

export async function mount(props) {
  console.log('[vue] props from main framework', props)
  globalRegister(store, props)
  render(props)
}

可以看到,该vuex模块在子应用mount时,会调用 initGlobalState将父应用下发的state初始化一遍,同时提供了 setGlobalState方法供外部调用,内部自动通知同步到父应用。子应用在vue页面使用时如下:

export default {
  computed: {
    ...mapState('global', {
      user: state => state.user, // 获取父应用的user信息
    }),
  },
  methods: {
    ...mapActions('global', ['setGlobalState']),
    update () {
        this.setGlobalState('user', { name: '张三' })
    }
  },
};

这样就达到了一个效果:子应用不用知道qiankun的存在,它只知道有这么一个global module可以存储信息,父子之间的通信都封装在方法本身了,它只关心本身的信息存储就可以了。

ps: 该方案也是有缺点的,由于子应用是在mount时才会同步父应用下发的state的。因此,它只适合每次只mount一个子应用的架构(不适合多个子应用共存);若父应用数据有变化而子应用又没触发mount,则父应用最新的数据无法同步回子应用。想要做到多子应用共存且父动态传子,子应用还是需要用到qiankun提供的 onGlobalStateChange的api监听才行,有更好方案的同学可以分享讨论一下。该方案刚好符合笔者当前的项目需求,因此够用了,请同学们根据自己的业务需求来封装。

子应用切换Loading处理

子应用首次加载时相当于新加载一个项目,还是比较慢的,因此loading是不得不加上的。

官方的例子中有做了loading的处理,但是需要额外引入 import Vue from 'vue/dist/vue.esm',这会增加主应用的打包体积(对比发现大概增加了100KB)。一个loading增加了100K,显然代价有点无法接受,所以需要考虑一种更优一点的办法。

我们的主应用是用vue搭建的,而且qiankun提供了 loader方法可以获取到子应用的加载状态,所以自然而然地可以想到: main.js中子应用加载时,将loading 的状态传给Vue实例,让Vue实例响应式地显示loading。接下来先选一个loading组件:

  • 如果主应用使用了ElementUI或其他框架,可以直接使用UI库提供的loading组件。
  • 如果主应用为了保持简单没有引入UI库,可以考虑自己写一个loading组件,或者找个小巧的loading库,如笔者这里要用到的 NProgress
npm install --save nprogress

接下来是想办法如何把loading状态传给主应用的 App.vue。经过笔者试验发现, new Vue方法返回的vue实例可以通过 instance.$children[0]来改变 App.vue的数据,所以改造一下 main.js

// 引入nprogress的css
import 'nprogress/nprogress.css'
import microApps from './micro-app';

// 获取实例
const instance = new Vue({
  render: h => h(App),
}).$mount('#app');

// 定义loader方法,loading改变时,将变量赋值给App.vue的data中的isLoading
function loader(loading) {
  if (instance && instance.$children) {
    // instance.$children[0] 是App.vue,此时直接改动App.vue的isLoading
    instance.$children[0].isLoading = loading
  }
}

// 给子应用配置加上loader方法
let apps = microApps.map(item => {
  return {
    ...item,
    loader
  }
})
registerMicroApps(apps);

start();
PS: qiankun的registerMicroApps方法也监听到子应用的beforeLoad、afterMount等生命周期,因此也可以使用这些方法记录loading状态,但更好的用法肯定是通过loader参数传递。

改造主应用的App.vue,通过watch监听 isLoading

<template><div id="layout-wrapper"><div class="layout-header">头部导航</div><div id="subapp-viewport"></div></div></template><script>
import NProgress from 'nprogress'
export default {
  name: 'App',
  data () {
    return {
      isLoading: true
    }
  },
  watch: {
    isLoading (val) {
      if (val) {
        NProgress.start()
      } else {
        this.$nextTick(() => {
          NProgress.done()
        })
      }
    }
  },
  components: {},
  created () {
    NProgress.start()
  }
}</script>

至此,loading效果就实现了。虽然 instance.$children[0].isLoading的操作看起来比较骚,但确实比官方的提供的例子成本小很多(体积增加几乎为0),若有更好的办法,欢迎大家评论区分享。

抽取公共代码

不可避免,有些方法或工具类是所有子应用都需要用到的,每个子应用都copy一份肯定是不好维护的,所以抽取公共代码到一处是必要的一步。

根目录下新建一个 common文件夹用于存放公共代码,如上面的多个vue子应用都可以共用的 global-register.js,或者是可复用的 request.jssdk之类的工具函数等。这里代码不贴了,请直接看 demo

公共代码抽取后,其他的应用如何使用呢? 可以让common发布为一个npm私包,npm私包有以下几种组织形式:

  • npm指向本地file地址: npm install file:../common。直接在根目录新建一个common目录,然后npm直接依赖文件路径。
  • npm指向私有git仓库: npm install git+ssh://xxx-common.git
  • 发布到npm私服。

本demo因为是基座和子应用都集合在一个git仓库上,所以采用了第一种方式,但实际应用时是发布到npm私服,因为后面我们会拆分基座和子应用为独立的子仓库,支持独立开发,后文会讲到。

需要注意的是,由于common是不经过babel和pollfy的,所以引用者需要在webpack打包时显性指定该模块需要编译,如vue子应用的vue.config.js需要加上这句:

module.exports = {
  transpileDependencies: ['common'],
}

子应用支持独立开发

微前端一个很重要的概念是拆分,是分治的思想,把所有的业务拆分为一个个独立可运行的模块。

从开发者的角度看,整个系统可能有N个子应用,如果启动整个系统可能会很慢很卡,而产品的某个需求可能只涉及到其中一个子应用,因此开发时只需启动涉及到的子应用即可,独立启动专注开发,因此是很有必要支持子应用的独立开发的。如果要支持,主要会遇到以下几个问题:

  • 子应用的登录态怎么维护?
  • 基座不启动时,怎么获取到基座下发的数据和能力?

在基座运行时,登录态和用户信息是存放在基座上的,然后基座通过props下发给子应用。但如果基座不启动,只是子应用独立启动,子应用就没法通过props获取到所需的用户信息了。因此,解决办法只能是父子应用都得实现一套相同的登录逻辑。为了可复用,可以把登录逻辑封装在common中,然后在子应用独立运行的逻辑中添加登录相关的逻辑。

// sub-vue/src/main.js

import { store as commonStore } from 'common'
import store from './store'

if (!window.__POWERED_BY_QIANKUN__) {
  // 这里是子应用独立运行的环境,实现子应用的登录逻辑
  
  // 独立运行时,也注册一个名为global的store module
  commonStore.globalRegister(store)
  // 模拟登录后,存储用户信息到global module
  const userInfo = { name: '我是独立运行时名字叫张三' } // 假设登录后取到的用户信息
  store.commit('global/setGlobalState', { user: userInfo })
  render()
}
// ...
export async function mount (props) {
  console.log('[vue] props from main framework', props)

  commonStore.globalRegister(store, props)

  render(props)
}
// ...

!window.__POWERED_BY_QIANKUN__表示子应用处于非 qiankun内的环境,即独立运行时。此时我们依然要注册一个名为 global的vuex module,子应用内部同样可以从global module中获取用户的信息,从而做到抹平qiankun和独立运行时的环境差异。

PS:我们前面写的 global-register.js写得很巧妙,能够同时支持两种环境,因此上面可以通过 commonStore.globalRegister直接引用。

子应用独立仓库

随着项目发展,子应用可能会越来越多,如果子应用和基座都集合在同一个git仓库,就会越来越臃肿。

若项目有CI/CD,只修改了某个子应用的代码,但代码提交会同时触发所有子应用构建,牵一发动全身,是不合理的。

同时,如果某些业务的子应用的开发是跨部门跨团队的,代码仓库如何分权限管理又是一个问题。

基于以上问题,我们不得不考虑将各个应用迁移到独立的git仓库。由于我们独立仓库了,项目可能不会再放到同一个目录下,因此前面通过 npm i file:../common方式安装的common就不适用了,所以最好还是发布到公司的npm私服或采用git地址形式。

qiankun-example为了更好展示,仍将所有应用都放在同一个git仓库下,请各位同学不要照抄。

子应用独立仓库后聚合管理

子应用独立git仓库后,可以做到独立启动独立开发了,这时候又会遇到问题: 开发环境都是独立的,无法一览整个应用的全貌

虽然开发时专注于某个子应用时更好,但总有需要整个项目跑起来的时候,比如当多个子应用需要互相依赖跳转时,所以还是要有一个整个项目对所有子应用git仓库的聚合管理才行,该聚合仓库要求做到能够一键install所有的依赖(包括子应用),一键启动整个项目。

这里主要考虑了三种方案:

  1. 使用 git submodule
  2. 使用 git subtree
  3. 单纯地将所有子仓库放到聚合目录下并 .gitignore掉。
  4. 使用lerna管理。

git submodulegit subtree都是很好的子仓库管理方案,但缺点是每次子应用变更后,聚合库还得同步一次变更。

考虑到并不是所有人都会使用该聚合仓库,子仓库独立开发时往往不会主动同步到聚合库,使用聚合库的同学就得经常做同步的操作,比较耗时耗力,不算特别完美。

所以第三种方案比较符合笔者目前团队的情况。聚合库相当于是一个空目录,在该目录下clone所有子仓库,并 gitignore,子仓库的代码提交都在各自的仓库目录下进行操作,这样聚合库可以避免做同步的操作。

由于ignore了所有子仓库,聚合库clone下来后,仍是一个空目录,此时我们可以写个脚本 scripts/clone-all.sh,把所有子仓库的clone命令都写上:

# 子仓库一
git clone git@xxx1.git

# 子仓库二
git clone git@xxx2.git

然后在聚合库也初始化一个 package.json,scripts加上:

"scripts": {"clone:all": "bash ./scripts/clone-all.sh",
  },

这样,git clone聚合库下来后,再 npm run clone:all就可以做到一键clone所有子仓库了。

前面说到聚合库要能够做到一键install和一键启动整个项目,我们参考qiankun的examples,使用 npm-run-all来做这个事情。

  1. 聚合库安装 npm i npm-run-all -D
  2. 聚合库的package.json增加install和start命令:
"scripts": {
    ..."install": "npm-run-all --serial install:*","install:main": "cd main && npm i","install:sub-vue": "cd sub-vue && npm i","install:sub-react": "cd sub-react && npm i","start": "npm-run-all --parallel start:*","start:sub-react": "cd sub-react && npm start","start:sub-vue": "cd sub-vue && npm start","start:main": "cd main && npm start"
  },
npm-run-all--serial表示有顺序地一个个执行, --parallel表示同时并行地运行。

配好以上,一键安装 npm i,一键启动 npm start

vscode eslint配置

如果使用vscode,且使用了eslint的插件做自动修复,由于项目处于非根目录,eslint没法生效,所以还需要指定eslint的工作目录:

// .vscode/settings.json
{"eslint.workingDirectories": ["./main","./sub-vue","./sub-react","./common"
  ],"eslint.enable": true,"editor.formatOnSave": false,"editor.codeActionsOnSave": {"source.fixAll.eslint": true
  },"search.useIgnoreFiles": false,"search.exclude": {"**/dist": true
  },
}

子应用互相跳转

除了点击页面顶部的菜单切换子应用,我们的需求也要求子应用内部跳其他子应用,这会涉及到顶部菜单active状态的展示问题: sub-vue切换到 sub-react,此时顶部菜单需要将 sub-react改为激活状态。有两种方案:

  • 子应用跳转动作向上抛给父应用,由父应用做真正的跳转,从而父应用知道要改变激活状态,有点子组件 $emit事件给父组件的意思。
  • 父应用监听 history.pushState事件,当发现路由换了,父应用从而知道要不要改变激活状态。

由于 qiankun暂时没有封装子应用向父应用抛出事件的api,如iframe的 postMessage,所以方案一有些难度,不过可以将激活状态放到状态管理中,子应用通过改变vuex中的值让父应用同步就行,做法可行但不太好,维护状态在状态管理中有点复杂了。

所以我们这里选方案二,子应用跳转是通过 history.pushState(null, '/sub-react', '/sub-react')的,因此父应用在mounted时想办法监听到 history.pushState就可以了。由于 history.popstate只能监听 back/forward/go却不能监听 history.pushState,所以需要额外全局复写一下 history.pushState事件。

// main/src/App.vue
export default {
  methods: {
    bindCurrent () {
      const path = window.location.pathname
      if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
        this.current = path
      }
    },
    listenRouterChange () {
      const _wr = function (type) {
        const orig = history[type]
        return function () {
          const rv = orig.apply(this, arguments)
          const e = new Event(type)
          e.arguments = arguments
          window.dispatchEvent(e)
          return rv
        }
      }
      history.pushState = _wr('pushState')

      window.addEventListener('pushState', this.bindCurrent)
      window.addEventListener('popstate', this.bindCurrent)

      this.$once('hook:beforeDestroy', () => {
        window.removeEventListener('pushState', this.bindCurrent)
        window.removeEventListener('popstate', this.bindCurrent)
      })
    }
  },
  mounted () {
    this.listenRouterChange()
  }
}

性能优化

每个子应用都是一个完整的应用,每个vue子应用都打包了一份 vue/vue-router/vuex。从整个项目的角度,相当于将那些模块打包了多次,会很浪费,所以这里可以进一步去优化性能。

首先我们能想到的是通过webpack的 externals或主应用下发公共模块进行复用。

但是要注意,如果所有子应用都共用一个相同的模块,从长远来看,不利于子应用的升级,难以两全其美。

现在觉得比较好的做法是:主应用可以下发一些自身用到的模块,子应用可以优先选择主应用下发的模块,当发现主应用没有时则自己加载;子应用也可以直接使用最新的版本而不用父应用下发的。

这个方案参考自 qiankun 微前端方案实践及总结-子项目之间的公共插件如何共享,思路说得非常完整,大家可以看看,本项目暂时还没加上该功能。

部署

现在网上qiankun部署相关的文章几乎搜不到,可能是觉得简单没啥好说的吧。但对于还不太熟悉的同学来说,其实会比较纠结qiankun部署的最佳部署方案是怎样的呢?所以觉得很有必要讲一下笔者这里的部署方案,供大家参考。

方案如下:

考虑到主应用和子应用共用域名时可能会存在路由冲突的问题,子应用可能会源源不断地添加进来,因此我们将子应用都放在 xx.com/subapp/这个二级目录下,根路径 /留给主应用。

步骤如下:

  1. 主应用main和所有子应用都打包出一份html,css,js,static,分目录上传到服务器,子应用统一放到 subapp目录下,最终如:
├── main
│   └── index.html
└── subapp
    ├── sub-react
    │   └── index.html
    └── sub-vue
        └── index.html
  1. 配置nginx,预期是 xx.com根路径指向主应用, xx.com/subapp指向子应用,子应用的配置只需写一份,以后新增子应用也不需要改nginx配置,以下应该是微应用部署的最简洁的一份nginx配置了。
server {
    listen       80;
    server_name qiankun.fengxianqi.com;
    location / {
        root   /data/web/qiankun/main;  # 主应用所在的目录
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    location /subapp {
        alias /data/web/qiankun/subapp;
        try_files $uri $uri/ /index.html;
    }

}

nginx -s reload后就可以了。

本文特地做了线上demo展示:

整站(主应用): http://qiankun.fengxianqi.com/

单独访问子应用:

遇到的问题

一、react子应用启动后,主应用第一次渲染后会挂掉

image
子应用的热重载居然会引得父应用直接挂掉,当时完全懵逼了。还好搜到了相关的 issues/340,即在复写react的webpack时禁用掉热重载(加了下面配置禁用后会导致没法热重载,react应用在开发时得手动刷新了,是不是有点难受。。。):

module.exports = {
  webpack: function override(config, env) {
    // 解决主应用接入后会挂掉的问题:https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter(
      (e) => !e.includes('webpackHotDevClient')
    );
    // ...
    return config;
  }
};

二、Uncaught Error: application 'xx' died in status SKIP_BECAUSE_BROKEN: [qiankun] Target container with #subapp-viewport not existed while xx mounting!

在本地dev开发时是完全正常的,这个问题是部署后在首次打开页面才会出现的,F5刷新后又会正常,只能在清掉缓存后复现一次。这个bug困扰了几天。

错误信息很清晰,即主应用在挂载xx子应用时,用于装载子应用的dom不存在。所以一开始以为是vue做主应用时, #subapp-viewport还没来得及渲染,因此要尝试确保主应用 mount后再注册子应用。

// 主应用的main.js
new Vue({
  render: h => h(App),
  mounted: () => {
    // mounted后再注册子应用
    renderMicroApps();
  },
}).$mount('#root-app');

但该办法不行,甚至setTimeout都用上了也不行,需另想办法。

最后逐步调试发现是项目加载了一段高德地图的js导致的,该js在首次加载时会使用 document.write去复写整个html,因此导致了#subapp-viewport不存在的报错,所以最后是要想办法去掉该js文件就可以了。

小插曲:为什么我们的项目会加载这个高德地图js?我们项目也没有用到啊,这时我们陷入了一个思维误区:qiankun是阿里的,高德也是阿里的,qiankun不会偷偷在渲染时动态加载高德的js做些数据收集吧?非常惭愧会对一个开源项目有这个想法。。。实际上,是因为我司写组件库模板的小伙伴忘记移除调试时 public/index.html用到的这个js了,当时还去评论 issue了(捂脸哭)。把这个讲出来,是想说遇到bug时还是要先检查一下自己,别轻易就去质疑别人。

最后

本文从开始搭建到部署非常完整地分享了整个架构搭建的一些思路和实践,希望能对大家有所帮助。要提醒一下的是,本示例可能不一定最佳的实践,仅作为一个思路参考,架构是会随着业务需求不断调整变化的,只有合适的才是最好的。

示例代码: https://github.com/fengxianqi/qiankun-example

在线demo: http://qiankun.fengxianqi.com/

单独访问在线子应用:

最后的最后,喜欢本文的同学还请能顺手给个赞和小星星鼓励一下,非常感谢看到这里。

一些参考文章

微服务拆分之道

$
0
0

背景

微服务在最近几年大行其道,很多公司的研发人员都在考虑微服务架构,同时,随着 Docker 容器技术和自动化运维等相关技术发展,微服务变得更容易管理,这给了微服务架构良好的发展机会。

在做微服务的路上,拆分服务是个很热的话题。我们应该按照什么原则将现有的业务进行拆分?是否拆分得越细就越好?接下来一起谈谈服务拆分的策略和坚持的原则。

拆分目的是什么?

在介绍如何拆分之前,我们需要了解下拆分的目的是什么,这样才不会在后续的拆分过程中忘了最初的目的。

拆分的本质是为了将复杂的问题简单化,那么我们在单体架构阶段遇到了哪些复杂性问题呢?首先来回想下当初为什么选用了单体架构,在电商项目刚启动的时候,我们只希望能尽快地将项目搭建起来,方便将产品更早的投放市场进行快速验证。在开发初期,这种架构确实给开发和运维带来了很大的便捷,主要体现在:
  • 开发简单直接,代码和项目集中式管理。
  • 排查问题时只需要排查这个应用就可以了,更有针对性。
  • 只需要维护一个工程,节省维护系统运行的人力成本。


但是随着功能越来越多,开发团队的规模越来越大,单体架构的缺陷慢慢体现出来,主要有以下几个方面:

在技术层面,数据库的连接数成为应用服务器扩容的瓶颈,因为连接 MySQL 的客户端数量是有限制的。

除此之外,单体架构增加了研发的成本抑制了研发效率的提升。比如公司的垂直电商系统团队会被按业务线拆分为不同的组。当如此多的小团队共同维护一套代码和一个系统时,在配合的过程中就会出现问题。不同的团队之间沟通少,假如一个团队需要一个发送短信的功能,那么有的研发同学会认为最快的方式不是询问其他团队是否有现成的,而是自己写一套,但是这种想法是不合适的,会造成功能服务的重复开发。由于代码部署在一起,每个人都向同一个代码库提交代码,代码冲突无法避免;同时功能之间耦合严重,可能你只是更改了很小的逻辑却导致其它功能不可用,从而在测试时需要对整体功能回归,延长了交付时间。模块之间互相依赖,一个小团队中的成员犯了一个错误,就可能会影响到其它团队维护的服务,对于整体系统稳定性影响很大。

最后,单体架构对于系统的运维也会有很大的影响。想象一下,在项目初期你的代码可能只有几千行,构建一次只需要一分钟,那么你可以很敏捷灵活地频繁上线变更修复问题。但是当你的系统扩充到几十万行甚至上百万行代码的时候,一次构建的过程包括编译、单元测试、打包和上传到正式环境,花费的时间可能达到十几分钟,并且任何小的修改,都需要构建整个项目,上线变更的过程非常不灵活。

而这些问题都可以通过微服务化拆分来解决。

为了方便你更好的理解这块,在此附上一份表格(内容来源:《持续演进的 Cloud Native:云原生架构下微服务最佳》一书),可以更直观地帮助你认识拆分的目的。
1.png

拆分时机应该如何决策?

产品初期,应该以单体架构优先。因为面对一个新的领域,对业务的理解很难在开始阶段就比较清晰,往往是经过一段时间之后,才能逐步稳定,如果拆分过早,导致边界拆分不合理或者拆的过细,反而会影响生产力。很多时候,从一个已有的单体架构中逐步划分服务,要比一开始就构建微服务简单得多。同时公司的产品并没有被市场验证过,有可能会失败,所以这个投入的风险也会比较高。

另外,在资源受限的情况下,采用微服务架构很多优势无法体现,性能上的劣势反而会比较明显。如下图所示。当业务复杂度达到一定程度后,微服务架构消耗的成本才会体现优势,并不是所有的场景都适合采用微服务架构,服务的划分应逐步进行,持续演进。产品初期,业务复杂度不高的时候,应该尽量采用单体架构。
2.png

随着公司的商业模式逐渐得到验证,且产品获得了市场的认可,为了能加快产品的迭代效率快速占领市场,公司开始引进更多的开发同学,这时系统的复杂度会变得越来越高,就出现单体应用和团队规模之间出现矛盾,研发效率不升反降。上图中的交叉点表明,业务已经达到了一定的复杂度,单体应用已经无法满足业务增长的需求,研发效率开始下降,而这时就是需要考虑进行服务拆分的时机点。这个点需要架构师去权衡。笔者所在的公司,是当团队规模达到百人的时候,才考虑进行服务化。

当我们清楚了什么时候进行拆分,就可以直接落地了吗?不是的,微服务拆分的落地还要提前准备好配套的基础设施,如服务描述、注册中心、服务框架、服务监控、服务追踪、服务治理等几大基本组件,以上每个组件缺一不可,每个组件展开又包括很多技术门槛,比如,容器技术、持续部署、DevOps 等相关概念,以及人才的储备和观念的变化。 微服务不仅仅是技术的升级,更是开发方式、组织架构、开发观念的转变。

至此,何时进行微服务的拆分,整体总结如下:
  • 业务规模:业务模式得到市场的验证,需要进一步加快脚步快速占领市场,这时业务的规模变得越来越大,按产品生命周期来划分(导入期、成长期、成熟期、衰退期)这时一般在成长期阶段。如果是导入期,尽量采用单体架构。
  • 团队规模:一般是团队达到百人的时候。
  • 技术储备:领域驱动设计、注册中心、配置中心、日志系统、持续交付、监控系统、分布式定时任务、CAP 理论、分布式调用链、API 网关等等。
  • 人才储备:精通微服务落地经验的架构师及相应开发同学。
  • 研发效率:研发效率大幅下降,具体问题参加上面拆分目的里提到的。


拆分时应该坚守哪些指导原则?

  • 单一服务内部功能高内聚低耦合,也就是说每个服务只完成自己职责内的任务,对于不是自己职责的功能交给其它服务来完成。
  • 闭包原则(CCP),微服务的闭包原则就是当我们需要改变一个微服务的时候,所有依赖都在这个微服务的组件内,不需要修改其他微服务。
  • 服务自治、接口隔离原则,尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏内部实现细节。这使得服务可以独立开发、测试、部署、运行,以服务为单位持续交付。
  • 持续演进原则,在服务拆分的初期,你其实很难确定服务究竟要拆成什么样。从微服务这几个字来看,服务的粒度貌似应该足够小,但是服务多了也会带来问题,服务数量快速增长会带来架构复杂度急剧升高,开发、测试、运维等环节很难快速适应,会导致故障率大幅增加,可用性降低,非必要情况,应逐步划分,持续演进,避免服务数量的爆炸性增长,这等同于灰度发布的效果,先拿出几个不太重要的功能拆分出一个服务做试验,如果出现故障,则可以减少故障的影响范围。
  • 拆分的过程尽量避免影响产品的日常功能迭代,也就是说要一边做产品功能迭代,一边完成服务化拆分。比如优先剥离比较独立的边界服务(如短信服务等),从非核心的服务出发减少拆分对现有业务的影响,也给团队一个练习、试错的机会。同时当两个服务存在依赖关系时优先拆分被依赖的服务。
  • 服务接口的定义要具备可扩展性,服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信了。在这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意想不到的错误。比如微服务的接口因为升级把之前的三个参数改成了四个,上线后导致调用方大量报错,推荐做法服务接口的参数类型最好是封装类,这样如果增加参数就不必变更接口的签名,而只需要在类中添加字段就可以了。
  • 避免环形依赖与双向依赖,尽量不要有服务之间的环形依赖或双向依赖,原因是存在这种情况说明我们的功能边界没有化分清楚或者有通用的功能没有下沉下来。
    3.png
  • 阶段性合并,随着你对业务领域理解的逐渐深入或者业务本身逻辑发生了比较大的变化,亦或者之前的拆分没有考虑的很清楚,导致拆分后的服务边界变得越来越混乱,这时就要重新梳理领域边界,不断纠正拆分的合理性。


拆分的粒度是不是越细越好?

目前很多传统的单体应用再向微服务架构进行升级改造,如果拆分粒度太细会增加运维复杂度,粒度过大又起不到效果,那么改造过程中如何平衡拆分粒度呢?

4.png

弓箭原理

平衡拆分粒度可以从两方面进行权衡,一是业务发展的复杂度,二是团队规模的人数。如上图,它就像弓箭一样,只有当业务复杂度和团队人数足够大的时候,射出的服务拆分粒度这把剑才会飞的更远,发挥出最大的威力。

比如说电商的商品服务,当我们把商品从大的单体里拆分出来的时候,就商品服务本身来讲,逻辑并没有足够复杂到 2~3 个人没法维护的地步,这时我们没有必要继续将商品服务拆的更细,但是随着业务的发展,商品的业务逻辑变的越来越复杂,可能同时服务公司的多个平台,此时你会发现商品服务本身面临的问题跟单体架构阶段面临的问题基本一样,这个阶段就需要我们将商品拆成更细粒度的服务,比如,库存服务、价格服务、类目服务、商品基础信息服务等等。

虽然业务复杂度已经满足了,如果公司此时没有足够的人力(招聘不及时或员工异动比较多),服务最好也不要拆分,拆分会因为人力的不足导致更多的问题,如研发效率大幅下降(一个开发负责与其不匹配数量的服务)。这里引申另外一个问题,一个微服务究竟需要几个开发维护是比较理性的?我引用下李云华老师在"从零开始学架构“ 中的一段经典论述,可以解决此问题。

三个火枪手原则

为什么说是三个人分配一个服务是比较理性的?而不是 4 个,也不是 2 个呢?

首先,从系统规模来讲,3 个人负责开发一个系统,系统的复杂度刚好达到每个人都能全面理解整个系统,又能够进行分工的粒度;如果是 2 个人开发一个系统,系统的复杂度不够,开发人员可能觉得无法体现自己的技术实力;如果是 4 个甚至更多人开发一个系统,系统复杂度又会无法让开发人员对系统的细节都了解很深。

其次,从团队管理来说,3 个人可以形成一个稳定的备份,即使 1 个人休假或者调配到其他系统,剩余 2 个人还可以支撑;如果是 2 个人,抽调 1 个后剩余的 1 个人压力很大;如果是 1 个人,这就是单点了,团队没有备份,某些情况下是很危险的,假如这个人休假了,系统出问题了怎么办?

最后,从技术提升的角度来讲,3 个人的技术小组既能够形成有效的讨论,又能够快速达成一致意见;如果是 2 个人,可能会出现互相坚持自己的意见,或者 2 个人经验都不足导致设计缺陷;如果是 1 个人,由于没有人跟他进行技术讨论,很可能陷入思维盲区导致重大问题;如果是 4 个人或者更多,可能有的参与的人员并没有认真参与,只是完成任务而已。

“三个火枪手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排 2 个人维护,每个人都可以维护多个微服务。

综上所诉,拆分粒度不是越细越好,粒度需要符合弓箭原理及三个火枪手原则。

拆分策略有哪些?

拆分策略可以按功能和非功能维度进行考虑,功能维度主要是划分清楚业务的边界,非功能维度主要考虑六点包括扩展性、复用性、高性能、高可用、安全性、异构性。接下来详细介绍下。

功能维度

功能维度主要是划分清楚业务边界,采用的主要设计方法可以利用 DDD(关于 DDD 的理论知识可以参考网上其它资料),DDD 的战略设计会建立领域模型,可以通过领域模型指导微服务的拆分,主要分四步进行:
  • 第一步,找出领域实体和值对象等领域对象。
  • 第二步,找出聚合根,根据实体、值对象与聚合根的依赖关系,建立聚合。
  • 第三步,根据业务及语义边界等因素,定义限界上下文。
  • 第四步,每一个限界上下文可以拆分为一个对应的微服务,但也要考虑一些非功能因素。


以电商的场景为例,交易链路划分的限界上下文如下图左半部分,根据一个限界上下文可以设计一个微服务,拆解出来的微服务如下图右侧部分。
5.png

非功能维度

当我们按照功能维度进行拆分后,并不是就万事大吉了,大部分场景下,我们还需要加入其它维度进一步拆分,才能最终解决单体架构带来的问题。
  • 扩展性:区分系统中变与不变的部分,不变的部分一般是成熟的、通用的服务功能,变的部分一般是改动比较多、满足业务迭代扩展性需要的功能,我们可以将不变的部分拆分出来,作为共用的服务,将变的部分独立出来满足个性化扩展需要。同时根据二八原则,系统中经常变动的部分大约只占 20%,而剩下的 80% 基本不变或极少变化,这样的拆分也解决了发布频率过多而影响成熟服务稳定性的问题。
  • 复用性:不同的业务里或服务里经常会出现重复的功能,比如每个服务都有鉴权、限流、安全及日志监控等功能,可以将这些通过的功能拆分出来形成独立的服务,也就是微服务里面的 API 网关。在如,对于滴滴业务,有快车和顺风车业务,其中都涉及到了订单支付的功能,那么就可以将订单支付独立出来,作为通用服务服务好上层业务。如下图:
    6.png
  • 高性能:将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其它服务。常见的拆分方式和具体的性能瓶颈有关,例如电商的抢购,性能压力最大的是入口的排队功能,可以将排队功能独立为一个服务。同时,我们也可以基于读写分离来拆分,比如电商的商品信息,在 App 端主要是商详有大量的读取操作,但是写入端商家中心访问量确很少。因此可以对流量较大或较为核心的服务做读写分离,拆分为两个服务发布,一个负责读,另外一个负责写。还有数据一致性是另一个基于性能维度拆分需要考虑的点,对于强一致的数据,属于强耦合,尽量放在同一个服务中(但是有时会因为各种原因需要进行拆分,那就需要有响应的机制进行保证),弱一致性通常可以拆分为不同的服务。
    7.png
  • 高可用:将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。具体拆分的时候,核心服务可以是一个也可以是多个,只要最终的服务数量满足“三个火枪手”的原则就可以。比如针对商家服务,可以拆分一个核心服务一个非核心服务,核心服务供交易服务访问,非核心提供给商家中心访问。
  • 安全性:不同的服务可能对信息安全有不同的要求,因此把需要高度安全的服务拆分出来,进行区别部署,比如设置特定的 DMZ 区域对服务进行分区部署,可以更有针对性地满足信息安全的要求,也可以降低对防火墙等安全设备吞吐量、并发性等方面的要求,降低成本,提高效率。
  • 异构性:对于对开发语言种类有要求的业务场景,可以用不同的语言将其功能独立出来实现一个独立服务。


以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合。 同时拆分不仅仅是架构上的调整,也意味着要在组织结构上做出相应的适应性优化,以确保拆分后的服务由相对独立的团队负责维护。

服务都拆了为什么还要合并?

古希腊哲学家赫拉克利特曾经说过:“人不能两次踏进同一条河流。”随着时间的流逝,任何事物的状态都会发生变化。线上系统同样如此,即使一个系统在不同时刻的状况也绝不会一模一样。现在拆分出来的服务粒度也许合适,但谁能保证这个粒度能够一直正确呢。

服务都拆了为什么还要合,就是要不断适应新的业务发展阶段,笔者这里做个类比看大家是否清晰,拆相当于我们开发代码,合相当于重构代码,为什么要重构呢,相信你肯定知道。微服务的合也是一样的道理,随着我们对应用程序领域的了解越来越深,它们可能会随着时间的推移而变化。例如,你可能会发现由于过多的进程间通信而导致特定的分解效率低下,导致你必须把一些服务组合在一起。

同时因为人员和服务数量的不匹配,导致的维护成本增加,也是导致服务合并的一个重要原因。例如,今年疫情的影响导致很多企业开始大量裁员,人员流失但是服务的数量确没有变,造成服务数量和人员的不平衡,一个开发同学同时要维护至少 5 个服务的开发,效率大幅下降。

那么如果微服务数量过多和资源不匹配,则可以考虑合并多个微服务到服务包,部署到一台服务器,这样可以节省服务运行时的基础资源消耗也降低了维护成本。需要注意的是,虽然服务包是运行在一个进程中,但是服务包内的服务依然要满足微服务定义,以便在未来某一天要重新拆开的时候可以很快就分离。服务合并到服务包示意图如下:
8.png

拆分过程中要注意的风险

  • 不打无准备之仗,开发团队是否具备足够的经验,能否驾驭微服务的技术栈,可能是第一个需要考虑的点。这里并不是要求团队必须具备完善的经验才能启动服务拆分,如果团队中有这方面的专家固然是最好的。如果没有,那可能就需要事先进行充分的技术论证和预演,至少不打无准备之仗。避免哪个简单就先拆哪个,哪个新业务要上了,先起一个服务再说。否则可能在一些分布式常见的问题上会踩坑,比如服务器资源不够、运维困难、服务之间调用混乱、调用重试、超时机制、分布式事务等等。
  • 不断纠正,我们需要承认我们的认知是有限的,只能基于目前的业务状态和有限的对未来的预测来制定出一个相对合适的拆分方案,而不是所谓的最优方案,任何方案都只能保证在当下提供了相对合适的粒度和划分原则,要时刻做好在未来的末一个时刻会变得不和时宜、需要再次调整的准备。因此随着业务的演进,需要我们重新审视服务的划分是否合理,如服务拆的太细,导致人员效率反而下降,故障的概率也大大增加,则需要重新划分好领域边界。
  • 要做行动派,而不是理论派,在具体怎么拆分上,也不要太纠结于是否合适,不动手怎么知道合不合适呢?如果拆了之后发现真的不合适,在重新调整就好了。你可能会说,重新调整成本比较高。但实际上这个问题的本质是有没有针对服务化架构搭建起一套完成的能力体系,比如服务治理平台、数据迁移工具、数据双写等等,如果有的话,重新调整的成本是不会太高的。


原文链接: https://mp.weixin.qq.com/s/mojOSgEUaHWGU3H3j7WjlQ

    linux grep 查看大日志文件

    $
    0
    0

    这是我参与更文挑战的第7天,活动详情查看: 更文挑战

    如果❤️我的文章有帮助,欢迎点赞、关注。这是对我继续技术创作最大的鼓励。

    linux grep 查看大日志文件

    场景

    今天隔离还在继续,在家办公。忽然下午午工作群里发来一个 mysql机器io/负载上升的预警,异常发生在 15:45 ~ 16:00之间。为了事后为了查明原因,需要翻看慢查询日志 slow.log才发现日志 8G 多... 故事就这样开始了 图片描述

    怎么办呢。第一个想到的就是常用 grep匹配关键字

    grep 关键字

    grep 常用于 关键字匹配文件文本信息。
    但关键字从哪里来呢,可以命令 head slow3306_9110.log查看下检索文件的 内容结构图片描述

    因为异常发生在 15:45 ~ 16:00之间,我就可以这样写

    grep -n 'Time: 210607 15:[45-59]' slow3306_9110.log

    时间 15:4515:59之间内容,但这样匹配只能看到时间,这明显不是我们想要的 图片描述

    grep 显示匹配行附近内容

    • A -> After
    • B -> Before
    • C -> Context

    举个例子:

    grep -A5 'Time: 210607 15:[45-59]' slow3306_9110.log

    就能把匹配 Time: 210607行的 下面 5 行也显示出来。 图片描述

    grep 多关键字搜索

    但这时我们有会发现, Query_time: 0.925375查询时间有大有小。我现在在查故障明细是只想看 查询消耗时间大的

    所以这里就需要用到 grep 多关键字搜索

    匹配多个关键字(且)

    管道符连接 多个条件实现关键字 且关系匹配:

    grep -A5 'Time: 210607 15:[45-59]' slow3306_9110.log | grep 'Query_time: (\d[2-5])'

    同一行同时满足两个条件( TimeQuery_time)才能够匹配。

    不过这里也必须说明: 因为上图内容格式中,Time 和 Query_time 不在同一列,所以上诉命令只是这个演示。实际只能匹配 同一行同时满足两个条件内容

    grep -E 匹配多个关键字(或)

    grep -E "word1|word2|word3" file.txt

    匹配文件中 同一行包含 word1、word2、word3 之一

    总结

    总结下来。

    • 由于多行无法同时命中 时间 15:45 至 15:59查询时间在 2~5位整数之间
    • 另外由于文件太大,grep 一次就能跑个 3、4 分钟实际体验并不好

    Oracle里收集与查看统计信息的方法_DBA Fighting!的技术博客_51CTO博客

    $
    0
    0

    Oracle数据库里的统计信息是这样的一组数据:它存储在数据字典里,且从多个维度描述了Oracle数据库里对象的详细信息。CBO会利用这些统计信息来计算目标SQL各种可能的、不同的执行路径的成本,并从中选择一条成本值最小的执行路径来作为目标SQL的执行计划。

    Oracle数据库里的统计信息可以分为如下6种类型:

    • 表的统计信息

    • 索引的统计信息

    • 列的统计信息

    • 系统统计信息

    • 数据字典统计信息

    • 内部对象统计信息

    表的统计信息用于描述Oracle数据库里表的详细信息,它包含了一些典型的维度,如记录数、表块(表里的数据块)数量、平均行长度等。

    索引的统计信息于描述Oracle数据库里索引的详细信息,它包含了一些典型的维度,如索引的层级、叶子块的数量、聚簇因子等。

    列的统计信息于描述Oracle数据库里列的详细信息,它包含了一些典型的维度,如列的distinct值的数量、列的NULL值的数量、列的最小值、列的最大值以及直方图等。

    系统统计信息于描述Oracle数据库所在的数据库服务器的系统处理能力,它包含了CPU和I/O这两个维度,借助于系统统计信息,Oracle可以更清楚地知道目标数据库服务器的实际处理能力。

    数据字典统计信息用于热核Oracle数据库里数据字典基表(如TAB$、IND$等)、数据字典基表上的索引,以及这些数据字典的列的详细信息,描述上述数据字典基表的统计信息与描述普通表、索引、列的统计信息没有本质区别。

    内部对象统计信息用于描述Oracle数据库里的一些内部表(如X$系列表)的详细信息,它的维度和普通表的统计信息的维度类似,只不过其表块的数量为0,因为X$系统表实际上只是Oracle自定义的内存结构,并不占用实际的物理存储空间。

    1、收集统计信息

    在Oracle数据库里,通常有两种方法可以用来收集统计信息:一种是使用ANALYZE命令;另一种是使用DBMS_STATS包。表、索引、列的统计信息和数据字典统计信息用ANALYZE命令或者DBMS_STATS包收集均可,但系统统计信息和系统内部对象统计信息只能使用DBMS_STATS包来收集。

    对系统内部表若使用ANALYZE命令来收集统计信息,会报错ORA-02030

    1.1用ANALYZE命令收集统计信息

    从Oracle7开始,ANALYZE命令就可以用来收集表、索引、列的统计信息,以及系统统计信息。

    典型用法如下:

    zx@ORCL>create table t2 as select * from dba_objects;
    
    Table created.
    
    zx@ORCL>create index idx_t2 on t2(object_id);
    
    Index created.
    
    zx@ORCL>analyze index idx_t2 delete statistics;
    
    Index analyzed.

    从Oracle 10g开始,创建索引后Oracle会怎么收集目标索引的统计信息,出现演示的目的,这里删除索引IDX_T2的统计信息:

    执行sosi脚本,从输出内容可以看到表T2、表T2的列和索引IDX_T2均没有相关的统计信息

    wKiom1iq7RLjGuX7AACqPsxAz2k185.png

    zx@ORCL>select count(*) from t2;
    
      COUNT(*)
    ----------
         86852

    只对表T2收集统计信息,并且以估算模式,采样的比例为15%:

    zx@ORCL>analyze table t2 estimate statistics sample 15 percent for table;
    
    Table analyzed.

    再次执行sosi脚本,可以看出现在只用表T2有统计信息,表T2的列和索引IDX_T2均没有相关的统计信息。而且因为采用的是估算模式所以估算结果和实际结果并不一定会完全匹配,比如表T2的实际数量与估算出的数量不一致。

    wKioL1iq7cTgWd0PAACu-EBdQjE462.png

    只对表T2收集统计信息,并且以计算模式:

    zx@ORCL>analyze table t2 compute statistics for table;
    
    Table analyzed.

    再次执行sosi脚本,可以看出现在只用表T2有统计信息,表T2的列和索引IDX_T2均没有相关的统计信息。而且因为采用的是计算模式,计算模式会扫描目标对象的所有数据,所以统计结果和实际结果是匹配的。

    wKioL1iq7iTD_PR5AACuQdP-vaQ696.png

    对表T2收集完统计信息后,现在对表T2的列OBJECT_NAME和OBJECT_ID以计算模式收集统计信息:

    zx@ORCL>analyze table t2 compute statistics for columns object_name,object_id;
    
    Table analyzed.

    再次执行sosi脚本,可以看出,现在列OBJECT_NAME和OBJECT_ID确实已经有统计信息了

    wKioL1iq7vuRFPShAACwW3OTl3E340.png

    注:在崔华老师的《基于Oracle的SQL优化》一书中提到T2原有的统计信息已经被抹掉了,也就是说对同一个对象而言,新执行的ANALYZE命令会抹掉之前ANALYZE的结果。但是在我实际的执行结果是表T2原有的统计信息没有被抹掉。我用到的环境是10.2.0.4和11.2.0.4,暂时没有11.2.0.1的环境。

    可以使用如下的命令同时以计算模式对表T2和列OBJECT_NAME、OBJECT_ID收集统计信息:

    zx@ORCL>analyze table t2 compute statistics for table for columns object_name,object_id;
    
    Table analyzed.

    再次执行sosi脚本,可以看到表T2和列OBJECT_NAME、OBJECT_ID上都有统计信息了。

    wKioL1iq7vuRFPShAACwW3OTl3E340.png

    使用如下命令可以以计算模式收集索引IDX_T2的统计信息

    zx@ORCL>analyze index idx_t2 compute statistics;
    
    Index analyzed.

    再次执行sosi脚本,从输出可以看到,现在索引IDX_T2已经有了统计信息,并且之前收集的表T2和列OBJECT_NAME、OBJECT_ID上的统计信息并没有被抹掉,这是因为我们刚才执行的ANALYZE命令和之前执行的ANALYZE命令针对的不是同一个对象。

    wKioL1iq79ixHno0AAC5g1rPsjY367.png

    使用如下命令可以删除表T2、表T2的所有列及表T2的所有索引的统计信息:

    zx@ORCL>analyze table t2 delete statistics;
    
    Table analyzed.

    再次执行sosi脚本,从输出可以看到,刚才收集的表T2、表T2的列OBJECT_NAME、OBJECT_ID以及索引IDX_T2的统计信息已经全部被删除了。

    wKiom1iq7RLjGuX7AACqPsxAz2k185.png

    如果想一次性以计算模式收集表T2、表T2的所有列和表T2上的所有索引的统计信息,执行如下的语句就可以了:

    zx@ORCL>analyze table t2 compute statistics;
    
    Table analyzed.

    再次执行sosi脚本,从输出可以看到,现在表T2、表T2的所有列和索引IDX_T2的统计信息都有了。

    wKioL1iq8njhcRkaAADiiUZNjSo403.png

    1.2用DBMS_STATS包收集统计信息

    从Oracle 8.1.5开始,DBMS_STATS包被广泛用于统计信息的收集,用DMBS_STATS包收集统计信息也是Oracle官方推荐的方式。在收集CBO所需要的统计信息方面,可以简单的将DBMS_STATS包理解成是ANALYZE命令的增加版。

    DBMS_STATS包里最常用的就是如下4个存储过程:

    • GATHER_TABLE_STATS:用于收集目标表、目标表的列和目标表上的索引的统计信息。

    • GATHER_INDEX_STATS:用于收集指定索引的统计信息。

    • GATHER_SCHEMA_STATS:用于收集指定schema下所有对象的统计信息。

    • GATHER_DATABASE_STATS:用于收集全库所有对象的统计信息。

    现在介绍DBMS_STATS包在收集统计信息时的常见用法,还是针对上面的测试表T2,这里使用DBMS_STATS包实现了和ANALYZE命令一模一样的效果。

    先删除表T2上的所有统计信息

    analyze table t2 delete statistics;

    只对表T2收集统计信息,并且以估算模式,采用的比例同样为15%:

    zx@ORCL>exec dbms_stats.gather_table_stats(ownname=>'ZX',tabname=>'T2',estimate_percent=>15,method_opt=>'FOR TABLE',cascade=>false);
    
    PL/SQL procedure successfully completed.

    执行sosi脚本,从输出内容可以看出,现在只有表T2有统计信息,表T2的列和索引IDX_T2均没有相关的统计信息。而且因为采用的估算模式,所以估算结果和实际结果并不一定会完全匹配。

    wKioL1iq8OGy7M0dAACvYkd_o6U368.png

    需要注意的是,这里Oracle数据库的版本是11.2.0.4,我们在调用DMBS_STATS.GATHER_TABLE_STATS时指定参数METHOD_OPT的值为'FOR TABLE',这表示只收集表T2的统计信息。这种收集表统计信息的方法并不适用于Oracle数据库所有的版本。例如这种方法就不适用于Oracle10.2.0.4和Oracle10.2.0.5,在这两个版本里,即使指定了'FOR TABLE',Oracle除了收集表统计信息之外还会对所有的列收集统计信息。

    如果公对表T2收集统计信息,并且是以计算模式收集,用DBMS_STATS包实现的方法就是将估算模式的采样比例(即参数ESTIMATE_PERCENT)设置为100%或NULL;

    exec dbms_stats.gather_table_stats(ownname=>'ZX',tabname=>'T2',estimate_percent=>100,method_opt=>'FOR TABLE',cascade=>false);

    exec dbms_stats.gather_table_stats(ownname=>'ZX',tabname=>'T2',estimate_percent=>NULL,method_opt=>'FOR TABLE',cascade=>false);

    zx@ORCL>exec dbms_stats.gather_table_stats(ownname=>'ZX',tabname=>'T2',estimate_percent=>100,method_opt=>'FOR TABLE',cascade=>false);
    
    PL/SQL procedure successfully completed.

    执行sosi脚本,从输出内容可以看出,现在只有表T2的统计信息,表T2的列和索引IDX_T2均没有相关的统计信息。而且因为采用的是计算模式,计算模式会扫描目标对象的所有数据,所以统计结果和实际结果是匹配的。

    wKiom1iq8T7gOu9fAACvLFuy1y0524.png

    对表T2收集完统计信息后,现在我们来对表T2的列OBJECT_NAME、OBJECT_ID以计算模式收集统计信息(不收集直方图):

    zx@ORCL>exec dbms_stats.gather_table_stats(ownname=>'ZX',tabname=>'T2',estimate_percent=>100,method_opt=>'for columns size 1 object_name,object_id',cascade=>false);
    
    PL/SQL procedure successfully completed.

    执行sosi脚本,从输出内容可以看出,现在表T2的列OBJECT_NAME、OBJECT_ID上都有统计信息了,并且Oracle还会同时收集表T2上的统计信息(注意,这和ANALYZE命令有所区别)。

    wKioL1iq8aKisiLnAAC0Sg2A2rA027.png

    使用如下命令可以以计算模式收集索引IDX_T2的统计信息

    zx@ORCL>exec dbms_stats.gather_index_stats(ownname=>'ZX',indname=>'IDX_T2',estimate_percent=>100);
    
    PL/SQL procedure successfully completed.

    执行sosi脚本,从输出内容可以看出,现在索引IDX_T2已经有了统计信息。

    wKioL1iq8gXjvnkQAAC5dsF0kDQ879.png

    使用如下命令可以删除表T2、表T2的所有列及表T2的所有索引的统计信息:

    zx@ORCL>exec dbms_stats.delete_table_stats(ownname=>'ZX',tabname=>'T2');
    
    PL/SQL procedure successfully completed.

    执行sosi脚本,从输出内容可以看出,表T2、表T2的所有列及表T2的所有索引的统计信息已经全部被删除了。

    wKiom1iq7RLjGuX7AACqPsxAz2k185.png

    如果想一次性以计算模式收集表T2、表T2的所有列及表T2的所有索引的统计信息,执行如下语句就可以了

    zx@ORCL>exec dbms_stats.gather_table_stats(ownname=>'ZX',tabname=>'T2',estimate_percent=>100,cascade=>true);
    
    PL/SQL procedure successfully completed.

    wKioL1iq8njhcRkaAADiiUZNjSo403.png

    1.3 ANALYZE和DBMS_STATS的区别

    从上面的演示中可以看出ANALYZE命令和DBMS_STATS包都可以用来收集表、索引和列的统计信息,看起来它们在收集统计信息方面的效果是一模一样的,为什么Oracle会推荐使用DBMS_STATS包来收集统计信息呢?

    因为ANALYZE命令和DMBS_STATS包相比,存在如下缺陷:

    ANALYZE命令不能正确地收集分区表的统计信息,而DBMS_STATS包却可以。ANALYZE命令只会收集最低层次对象的统计信息,然后推导和汇总出高一级的统计信息,比如对于有子分区的分区表而言,它只会先收集子分区统计信息,然后再汇总,推导出分区或表级的统计信息。有的统计信息是可以从当前对象的下一级对象进行汇总后得到的,比如表的总行数,可以由各分区的行数相加得到。但有的统计信息则不能从下一级对象得到,比如列上的distinct值数量NUM_DISTINCT以及DESNSITY等。

    ANALYZE命令不能并行收集统计信息,而DBMS_STATS包却可以。并行收集统计信息对数据量很大的表表而言,是非常有用的特性。对于数据量很大的表,如果不能并行收集统计信息,则意味着如果想精确地收集目标对象的统计信息,那么耗费的时间可能会非常长,这有可能是不能接受的。在Oracle数据库里,DBMS_STATS包收集统计信息可以并行执行,这在一定程度上缓解了对大表的统计信息收集过长所带来的一系列问题。

    DBMS_STATS包的并行收集是通过手工指定输入参数DEGREE来实现的,比如对表T1进行收集统计信息,同时指定并行度为4:

    exec dbms_stats.gahter_table_stats(ownname=>'SCOTT',tabname=>'T1',cascade=>true,estimate_percent=>100,degree=>4);

    当然,DBMS_STATS包也不是完美的,它与ANALYZE命令相比,其缺陷在于DBMS_STATS包只能收集与CBO相关的统计信息,而与CBO无关的一些额外信息,比如行迁移/行链接的数量(CHAIN_CNT)、校验表和索引的结构信息等,DBMS_STATS包就无能为力了。而ANALYZE命令可以用来分析和收集上述额外的信息,比如analyze table xxx list chained rows intoyyy可以用来分析和收集行迁移/行链接的数量,analyzeindex xxx validate structure可以用来分析索引的结构。

    2、查看统计信息

    前面介绍了如何收集统计信息,那如何查看这些统计信息呢?Oracle数据库的统计信息会存储在数据字典里,我们只需要去查询相关的数据字典就好了。如果有充裕的时间,现写SQL去查询数据字典里的统计信息也没有什么,但当我们真正碰到有性能问题的SQL时,通常会希望能在第一时间就收集到与目标SQL相关的各种统计信息,以便于在第一时间定位问题所在,这时候写SQL去查询数据字典就已经来不及了,所以我们需要事先准备好通用的查询统计信息的脚本,出问题的时候只需要运行一下脚本,就能在第一时间获取目标对象的所有统计信息了。

    sosi脚本(Show Optimizer Statistics Information)就是这样一种脚本,国内的Oracle数据库专家也一直在用这个脚本,它源于MOS上的文章:SCRIPT - Select to show OptimizerStatistics for CBO (文档ID 31412.1),用法很简单,只需要运行一下sosi脚本,并指定要查看统计信息的表名就可以了。它支持分区表,显示分为三部分,分别是表级别的统计信息,分区级别的统计信息和子分区级别的统计信息。前面做实验用到的也是这个脚本。

     附件是sosi脚本可以下载使用。

    参考《基于Oracle的SQL优化》

    Oracle 统计信息收集 - Leohahah - 博客园

    $
    0
    0

    官网网址参考:

    https://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_stats.htm#CIHBIEII

    https://docs.oracle.com/cd/B12037_01/server.101/b10759/statements_4005.htm#i2150533

    https://asktom.oracle.com/pls/asktom/f?p=100:11:::::p11_question_id:5792247321358

    https://docs.oracle.com/database/121/TGSQL/tgsql_histo.htm#TGSQL366

    查询表上一次收集统计信息的时间:

    1
    select  owner,table_name,last_analyzed from  dba_tables where  owner='SCOTT';

    统计信息涉及的视图:

    Column statistics appear in the data dictionary views  USER_TAB_COLUMNSALL_TAB_COLUMNS, and  DBA_TAB_COLUMNS. Histograms appear in the data dictionary views  USER_TAB_HISTOGRAMSDBA_TAB_HISTOGRAMS, and  ALL_TAB_HISTOGRAMSUSER_PART_HISTOGRAMSDBA_PART_HISTOGRAMS, and  ALL_PART_HISTOGRAMS; and  USER_SUBPART_HISTOGRAMSDBA_SUBPART_HISTOGRAMS, and  ALL_SUBPART_HISTOGRAMS.

    收集统计信息主要有2种方法:

    1. analyze

    analyze可以用来收集表,索引,列以及系统的统计信息和直方图,以下为一些典型用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    analyze table  scott.emp compute statistics--收集所有的统计信息和直方图信息,包括表、列、索引。
    analyze table  scott.emp compute statistics  for  table--收集emp表的统计信息,不含列、索引统计信息和直方图。
    analyze table  scott.emp compute statistics  for  all  columns;  --收集所有列的统计信息和直方图(超大表较耗资源,因为只要列中有非空值,那么就会收集这个列的统计信息和直方图)。
    analyze table  scott.emp compute statistics  for  all  indexed columns;  --收集所有索引列的统计信息和直方图。
    analyze table  scott.emp compute statistics  for  all  indexes; --收集所有索引统计信息,不含列的统计信息和直方图。
    analyze table  scott.emp compute statistics  for  columns 列1,列2; --收集2个列的统计信息和直方图。
    analyze index  idx_ename delete  statistics--删除索引idx_ename的统计信息。
    analyze table  scott.emp delete  statistics--删除表t1所有的表,列,索引的统计信息和列直方图。
    analyze table  scott.emp estimate statistics  sample 15 percent for  table--收集emp表的统计信息,以估算模式采样比例为15%进行收集,不含列、索引统计信息和直方图。

    从语法可以看出,只有指定列统计信息收集时,才会收集相关列的直方图,此外收集直方图时for子句还可以加size子句,size的取值范围是1-254,默认值是75,表示直方图的buckets的最大数目。而dbms_stats包的size选择则有:数字|auto|repeat|skewonly选项,但analyze的size只能是数字。

     

    关于直方图:

    A histogram is a special type of column statistic that provides more detailed information about the data distribution in a table column. A histogram sorts values into "buckets," as you might sort coins into buckets.

    从官网解释(参考第四个网址)来看,直方图就是一种特殊的列统计信息,这也与我们上边的推断相符,只有列才有直方图。

    这里贴一个Tom Kyte用于查看analyze后统计信息的SQL:(已稍作改进,仅示例,这种格式的SQL不推荐,原SQL较简单参考第三个网址)

    1
    2
    3
    4
    5
    select  t.num_rows as  num_rows_in_table, i.index_name, i.num_rows as  num_rows_in_index, co.num_analyzed_cols,ch.histogram_cnt
    from  (select  num_rows from  user_tables where  table_name ='EMP') t,
         (select  index_name,num_rows from  user_indexes where  table_name = 'EMP') i,
         (select  count(*) as  num_analyzed_cols from  user_tab_columns where  table_name='EMP'  and  num_distinct is  not  null) co,
         (select  count(distinct  column_name) histogram_cnt from  user_tab_histograms where  table_name = 'EMP'  ) ch;

    需要注意的一点是for table选项在某些版本中并不只收集表统计信息,而是连列和索引的统计信息一块收集了,至于具体哪些版本的表现不同这里不做深究,使用上述SQL可以轻易的测试出你的analyze和dbms_stats语句到底收集了什么统计信息和直方图。

     

    2. 调用dbms_stats包

    dbms_stats与analyze的区别是:

    analyze收集系统内部对象会报错,而dbms_stats不会

    analyze不能正确的收集分区表的统计信息 而dbms_stats可以通过指定粒度来实现(granularity)。

    analyze不能并行的收集统计信息,而dbms_stats可以(可以加上degree=>4来实现并行度为4的收集)。

    Oracle推荐使用dbms_stats来收集统计信息,analyze将会被逐渐抛弃。

    dbms_stats中负责收集统计信息的是以下几个存储过程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    GATHER_DATABASE_STATS
        --This procedure gathers statistics for all objects in the database.
    GATHER_DICTIONARY_STATS
        --This procedure gathers statistics for dictionary schemas 'SYS', 'SYSTEM' and schemas of RDBMS components.
    GATHER_FIXED_OBJECTS_STATS
        --This procedure gathers statistics for all fixed objects (dynamic performance tables).
    GATHER_INDEX_STATS
        --This procedure gathers index statistics. It attempts to parallelize as much of the work as possible. Restrictions are described in the individual parameters. This operation will not parallelize with certain types of indexes, including cluster indexes, domain indexes, and bitmap join indexes. The granularity and no_invalidate arguments are not relevant to these types of indexes.
    GATHER_SCHEMA_STATS
        --This procedure gathers statistics for all objects in a schema.
    GATHER_SYSTEM_STATS
        --This procedure gathers system statistics.
    GATHER_TABLE_STATS
        --This procedure gathers table and column (and index) statistics. It attempts to parallelize as much of the work as possible, but there are some restrictions as described in the individual parameters.

    三个常用Procedure用法详解:GATHER_SCHEMA_STATS(两种用法)、GATHER_TABLE_STATS、GATHER_INDEX_STATS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    PROCEDURE  GATHER_SCHEMA_STATS
     Argument Name           Type            In/Out  Default?
     ------------------------------ ----------------------- ------ --------
     OWNNAME            VARCHAR2        IN
     ESTIMATE_PERCENT   NUMBER          IN      DEFAULT
     BLOCK_SAMPLE       BOOLEAN         IN      DEFAULT
     METHOD_OPT         VARCHAR2        IN      DEFAULT
     DEGREE             NUMBER          IN      DEFAULT
     GRANULARITY        VARCHAR2        IN      DEFAULT
     CASCADE             BOOLEAN         IN      DEFAULT
     STATTAB            VARCHAR2        IN      DEFAULT
     STATID             VARCHAR2        IN      DEFAULT
     OPTIONS            VARCHAR2        IN      DEFAULT
     OBJLIST            OBJECTTAB       OUT
     STATOWN            VARCHAR2        IN      DEFAULT
     NO_INVALIDATE      BOOLEAN         IN      DEFAULT
     GATHER_TEMP        BOOLEAN         IN      DEFAULT
     GATHER_FIXED       BOOLEAN         IN      DEFAULT
     STATTYPE           VARCHAR2        IN      DEFAULT
     FORCE               BOOLEAN         IN      DEFAULT
     OBJ_FILTER_LIST    OBJECTTAB       IN      DEFAULT
     
    PROCEDURE  GATHER_SCHEMA_STATS
     Argument Name           Type            In/Out  Default?
     ------------------------------ ----------------------- ------ --------
     OWNNAME            VARCHAR2        IN
     ESTIMATE_PERCENT   NUMBER          IN      DEFAULT
     BLOCK_SAMPLE       BOOLEAN         IN      DEFAULT
     METHOD_OPT         VARCHAR2        IN      DEFAULT
     DEGREE             NUMBER          IN      DEFAULT
     GRANULARITY        VARCHAR2        IN      DEFAULT
     CASCADE             BOOLEAN         IN      DEFAULT
     STATTAB            VARCHAR2        IN      DEFAULT
     STATID             VARCHAR2        IN      DEFAULT
     OPTIONS            VARCHAR2        IN      DEFAULT
     STATOWN            VARCHAR2        IN      DEFAULT
     NO_INVALIDATE      BOOLEAN         IN      DEFAULT
     GATHER_TEMP        BOOLEAN         IN      DEFAULT
     GATHER_FIXED       BOOLEAN         IN      DEFAULT
     STATTYPE           VARCHAR2        IN      DEFAULT
     FORCE               BOOLEAN         IN      DEFAULT
     OBJ_FILTER_LIST    OBJECTTAB       IN      DEFAULT
      
    PROCEDURE  GATHER_TABLE_STATS
     Argument Name           Type            In/Out  Default?
     ------------------------------ ----------------------- ------ --------
     OWNNAME            VARCHAR2        IN
     TABNAME            VARCHAR2        IN
     PARTNAME           VARCHAR2        IN      DEFAULT
     ESTIMATE_PERCENT   NUMBER          IN      DEFAULT
     BLOCK_SAMPLE       BOOLEAN         IN      DEFAULT
     METHOD_OPT         VARCHAR2        IN      DEFAULT
     DEGREE             NUMBER          IN      DEFAULT
     GRANULARITY        VARCHAR2        IN      DEFAULT
     CASCADE             BOOLEAN         IN      DEFAULT
     STATTAB            VARCHAR2        IN      DEFAULT
     STATID             VARCHAR2        IN      DEFAULT
     STATOWN            VARCHAR2        IN      DEFAULT
     NO_INVALIDATE      BOOLEAN         IN      DEFAULT
     STATTYPE           VARCHAR2        IN      DEFAULT
     FORCE               BOOLEAN         IN      DEFAULT
      
    PROCEDURE  GATHER_INDEX_STATS
     Argument Name           Type            In/Out  Default?
     ------------------------------ ----------------------- ------ --------
     OWNNAME            VARCHAR2        IN
     INDNAME            VARCHAR2        IN
     PARTNAME           VARCHAR2        IN      DEFAULT
     ESTIMATE_PERCENT   NUMBER          IN      DEFAULT
     STATTAB            VARCHAR2        IN      DEFAULT
     STATID             VARCHAR2        IN      DEFAULT
     STATOWN            VARCHAR2        IN      DEFAULT
     DEGREE             NUMBER          IN      DEFAULT
     GRANULARITY        VARCHAR2        IN      DEFAULT
     NO_INVALIDATE      BOOLEAN         IN      DEFAULT
     STATTYPE           VARCHAR2        IN      DEFAULT
     FORCE               BOOLEAN         IN      DEFAULT

    GATHER_SCHEMA_STATS参数详解:(其他存储过程的参数解释参见官方页面,很多参数description都是通用的)

    详见: Table 103-30 GATHER_SCHEMA_STATS Procedure Parameters

    一些实际用例:

    1
    2
    3
    EXEC  DBMS_STATS.GATHER_SCHEMA_STATS('SCOTT',estimate_percent=>80,method_opt=>'FOR ALL COLUMNS SIZE AUTO',degree=>4,cascade=>TRUE);
    EXEC  DBMS_STATS.GATHER_TABLE_STATS('SCOTT','EMP',estimate_percent=>80,method_opt=>'FOR ALL COLUMNS SIZE AUTO',degree=>4,cascade=>TRUE);
    EXEC  DBMS_STATS.GATHER_INDEX_STATS('SCOTT','PK_EMP',estimate_percent=>80,degree=>4);

    一些特别提示:

    • 虽然method_opt的description中并未提及for table这个选项,但其实这个选项也是有效的,同analyze一样,这个参数在不同版本的表现也是不一样的,具体差异也可以轻易的使用本文中提供的SQL观察到。
    • 如果想使用compute方式收集统计信息,将estimate_percent设为100或者null即可。
    • Oracle有auto optimizer stats collection的自动维护任务定期的收集统计信息,这些任务是默认开启的,但当数据库变的很大之后就会引发严重的性能问题,建议只保留周末的一个窗口,其他窗口全部关闭。
    • 直方图统计信息并不是那么的重要,只有在遇到对倾斜列(skew)的查询很频繁时才有用,这种情况并不常见。
    • 不再推荐使用analyze来收集统计信息,除非是做测试或者表很小,dbms_stats的并行度选项能加快收集速度。
    • 对大表采样收集统计信息时一般采样比例不需要很大,通常10%到30%即可,如果业务可以提供维护窗口,那100%也没什么大不了。
    • 如果要详细了解统计信息收集了什么内容,可以参考本文提供的网址链接和视图。

    关于执行权限:

    To invoke this procedure you must be owner of the table, or you need the ANALYZE ANY privilege. For objects owned by SYS, you need to be either the owner of the table, or you need the ANALYZE ANY DICTIONARY privilege or the SYSDBA privilege.

    当然关于权限还有个取巧的办法,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    conn hr/hr
    create  or  replace  procedure  gather_stats is
    begin 
    dbms_stats.gather_table_stats('HR''EMPLOYEES');
    end  gather_stats;
    grant  select  on  hr.employees to  scott;
    grant  execute  on  gather_stats to  scott; 
    conn scott/scott  exec  hr.gather_stats;

    鼓起勇气从体制内辞职,分享一些经验教训感悟 - 找工作啦(Job)版 - 北大未名BBS

    $
    0
    0

    (2021.6.8更新:

    很多学弟学妹给我发私信咨询,谢谢大家的信任。我也是一个普通人,站得不高,也看得不远,如有未解答的问题请见谅。如果有任何问题欢迎私信与我交流,知无不言。也希望其他同学分享各种求职行业经验和思考,造福学弟学妹。希望出现赞数更多的文章😄

    帖子热度慢慢降下去了,很开心大家有了充分的讨论。这也是我发帖提醒各位的原因。能让大家有一些思考,有些观照就很好了。至少不会是社会绝大多数人那样被结婚生子买房买车的道路裹挟。最后还想说,希望大家做好规划,勤于思考。去做一个幸福的普通人,但一定更要有内心的坚守和追求,不愧对在北大的学习时光和“眼底未名水,胸中黄河月”的情怀。


    (2021.6.7更新:

    发现自己写的帖子引起了大家这么热烈的讨论,真的很感谢师弟师妹们对我的鼓励,也有许多同龄人相似经历的人分享自己的心路历程,非常感动。也看到有体制内的前辈批评我,劝导我,我都虚心接受。我还不到30,道行尚浅,没有体制内的前辈站得高,看得远。选择都是自己做出来的,既然做了,就不会后悔。

    我尽量用一种客观的语言陈述事实,而不是进行价值判断,可能还没有做到完全客观。我再次强调我没有否定体制内的任何意思,只是真实体验过冷暖自知。能引发大家的思考和讨论就很好,没有任何劝戒的观点。择业是人生大事,只是希望大家根据自己的情况谨慎决策,慎重思考,这大有裨益。我没有在体制外呆过,资历尚浅,只能就自己的经验做一些分享,说的不周的地方请见谅。这几千字的内容是我周末有感而发,回想到学生时代的一些错误和没有注意到的地方,更多是面向在校生和毕业生的提示和分享。更多前辈比我更有经验阅历,请不要苛责我作为一个后辈稚嫩的观点。开放的平台大家都可以讨论、分享。

    非常感谢北大bbs,我们能有一个深度、平等、开放、安全的交流空间。兼听则明,偏信则暗,我也在各位的回复中收获颇丰。另烦请各位前辈和学弟学妹不要转载,让我们停留在北大这个开放包容的环境中讨论,以免在社会舆论引起不必要的纷扰。

    我在园子里度过了人生最美好的6年,热爱这里的一草一木、一砖一瓦。谢谢大家的关心,祝大家都有美好的前程。



    原帖如下:


    终于鼓起勇气从体制内辞职了,看着即将入职的小朋友踌躇满志,想到了五年前的我,也是如此对工作和未来充满了期待而意气风发。即将迎接我的将是一个未知的领域,充满风险和挑战的社会。在此我复盘我踩过的坑和没有注意的地方,希望能为学弟学妹做些提示,做好职业生涯规划,完成学生到社会的蜕变。

     

    我是p大理工本金融类硕,现在在某金融类体制内单位工作了五年。当时该单位(以下简称甲单位)招聘很好,我放弃了其他的offer来到甲单位。此后便开始了我的体制内生涯。


    到单位的第一天,我还未脱去学生的稚嫩,领导语重心长地对我说:“大家都很优秀,不过年轻同事肯定要从最基础的做起。“我牢记父母对我的教诲,进体制内单位低头做事,谨言慎行,放低身段。没想各种工作还是远远超出了我的预期。“第一年你们小年轻就做做基本工作就行“……我不由得先吃了一惊。此后,我发现我再也和债券股票、市场基本面、财务报表、宏观经济有任何关系,取而代之的是巡视、审计、保密、写文、送文、改文、核稿、党建、团建、订会议室、摆座牌、准备材料、发言稿、不忘初心、调格式、设置页码、检查错别字、认真学习“请XX同志阅示、阅研、阅批、办理、加急办理、高度重视”的批示精神……


    体制内就是一个机器,每个人就是一个螺丝钉。随着内卷程度的加深,原本中专生干的活现在需要北大清华的学生来干,对,如果你学校不够好连这体制内单位的门都进不去。领导其实有时候也在说“体制内浪费了多少人才”。诚然,我在服务领导的路上一直走过了五年。第一年还在适应的过程中,第二年第三年在恐惧自己要被社会淘汰的焦虑中和在毫无意义的工作中埋没,当然后面还有疫情,更让我踟蹰不前。


    我终于决定要出来,这并不是我想要的,更不是我当时来甲单位的初心。社会现实再次给了我冷冷一击。有着cfa的我投的简历依然石沉大海,毕竟离市场太远了这么多年,早已荒废了各类知识。实在走投无路,靠学长学姐推荐了几个单位,面试的时候看了简历,共同感叹“太可惜了,你的学历和软件实力完全没问题,都是顶尖。但是你的工作经历不能和任何工作匹配,这是硬伤”“你到我这里来也是给你按刚入职的新生算”“你虽然在体制内,但是你能给我们带来什么资源呢,我们招你和应届生,其实没有本质的差别”“你就算北大毕业,我们还是会录用更有经验的人”……


    凡此种种,再一次让我认识到了社会的残酷。我面临着工资的大幅下跌,面临着周围同批同学早已在自己的岗位上飞速成长成为骨干甚至带起了自己的团队的这种心理落差。有时候常常和同学开玩笑:“再有一批国企下岗,那一定是我下岗。”虽然是玩笑话,背后确是说不尽的辛酸。


    不过无所谓,已经选择了重头开始。我在这里还是复盘一下自己做得不好的地方,给学弟学妹一些提醒:

     

    一、  一些犯过的错误

    1. 未能理解选人标准高不等于育人标准高

    很多时候大家offer求比较更多看的是这个单位如何,但实际上更重要的是你所在的岗位如何。单位固然也很重要,但要明白,什么东西脱离了你的单位,成为你自己的东西,这才是你自我价值的提升。按在现在内卷的情况,一窝蜂北大清华的学生都涌进体制内或部分机构,他们选人的标准自然很高。正如领导所言什么岗位都要有做最基础的打印复印扫描文件,只是现在轮到你们北大的人做而已。不要秉持学生思维认为什么地方难进就一定是好的单位,更好看这个单位里你从事的工作和单位对你的培养。

     

    2. 未能树立明确职业规划和人生道路有效衔接

    我直到本科后才知道去实习,凭借理工科的背景也是进入金融机构较为顺利。但是我一直不知自己的职业规划,或者通俗一点,自己的“爽点”在什么地方。有和我背景类似的同学瞄着基金经理的岗位专注二级市场,也有专注编程技能成为很好的金融建模专家。(当然,他们工作压力和竞争也很大)而我缺少这样的规划,自然当时觉得什么地方难进就什么地方。同时,很多来甲单位的同事冲着结婚生子就去了,迅速办完了人生大事再搞后续的事业。诚然,我缺少了对长期人生家庭的各类规划。

     

    3.  未能正确理解体制内工作内容核心就是写公文

    尽管甲单位是接触市场,但不妨碍这就是一个体制内机构。体制内就是写公文。说实话,这个我之前从未接触过。而我在体制内发展得还不错的同学都是一直知道自己要写公文,并且接受现在的工作状态。每天在”高度“”密切“”深入“”切实““提高”“巩固”“充实”“稳中有增”中来回选择,领导再把你的把字句变成被字句,一般改个10几遍都很正常,当然最后可能改回了原来的样子。不断揣摩领导的意思和思路,互相猜来猜去,也说不明白具体的内容。

    所有想去体制内的同学们不妨问自己三个问题:

    1)  能否接受每天按人民日报、求是、或者高中政治的口吻语调写东西

    2)  能否接受像照顾婴儿一样无微不至服务领导、提高政治站位、进行各类学习活动

    3)  能否接受长期做一种工作直到退休,肉眼可见自己的能力慢慢退化,而周围同学活跃而心如止水

    如果这三个问题有一个你表示不能接受,那慎重考虑你的选择。

     

    4.  未能有效理解“体制内“的很多其他的影响

    体制内和体制外完全是不同的两套逻辑,双方互不相认。体制外不认可体制内做的事,而想从体制外跳进体制内却很难很难。

    一是体制内(除了少数专业性岗位)是一个去能力化的地方,我虽然学习金融,但我去公安部、外交部、农业部、科技部一点问题都没有。因为就是要写公文,执行领导意图。

    二是体制内整个思维的比社会晚10年,进了这里你就和市场彻底远离,很多领导甚至不知道什么叫ETF,什么叫注册制,再老一点的领导连Excel的筛选都不会。整体思维也非常老,年轻人提出一点新观点新思路,领导会因为没听过或者求稳而直接pass。

    三是这是一个忠诚大于能力的地方,很多晋升等靠的不是你能力有多强,而是你多懂得做人和把握机遇。学校(特别是北大)里培养的批判性思维,创新性思维简直就是体制内的大忌。某领导亲自说,你们北大的人太难管,心高气傲,想法还这么多,只管做就行了。很多高学历的人在体制内待不住,就是因为看到能力比自己差的人反而得到了提升,咽不下这口气。

    四是体制内还有很多副产品,比如“非必要不出京”,强制疫苗,各种批准,对于习惯了北大自由懒散环境的人简直就是戴上了紧箍咒。

    五是千万别要高估北大学历,北大名声不是很好,多冠以懒散,精致利己等名号。同时,只要是硕士,无论是北大、西大,还是西北大,无论是什么大学就是“硕士研究生”,哪怕一个更差的大学的硕士也比北大本科好,同理,更差的大学的博士也比北大硕士好。这也是为什么体制内的人一定想去读在职博士,无论什么学校什么专业有多水,就是要博士这个title。

    六是要习惯很多你根本之前很难接受的东西,无论是金融国企还是制造业国企,做什么不重要,重要的是萝卜坑和位置。所以为了解决位置和待遇,经常出现外行指挥内行的情况。行政领导调来做研究,法律领导调去管人力,办公厅秘书出来搞编程,都是很常见的现象。

     

    5.  规划滞后,缺少与市场的了解

    原来我的设想是在体制内出来可以获得很多信息和人脉,也锻炼了规矩。然而从体制内再出来这个路早已经是过去式了。

    一是日益趋严的监管让那些体制内的人有很多行业禁令。

    二是很多市场机构的坑位都已经被早跳出体制内的领导们占满了,他们大多业务处室有丰富的监管经验,小年轻没法比。

    三是我们都太渺小,根本接触不到政策,可能到你这里就是读政策的一句话,离政策制定、了解政策实在是太远了。

    四是所谓的人脉就是虚的,别人认的是这个单位,而不是你这个人。

    我们经常有一个幸存者偏差的误区,看到任泽平等从体制内历练多年而跳出来的大佬们觉得这也可以是自己发展的路径。殊不知,这是一个幸存者偏差,每一个人都想这么走,但必须要有各种加持和机遇才能实现他们如今的成就。

     

    二、一些建议

    1.  对父母和老师的建议兼听则明

    楼主家在某一线城市,即使这样,父母的观念(包括其他很多同龄人的父母)大多还是体制内最好,体制内万岁。一定要考公务员,不行就去事业单位,再不行央企,实在不行地方国企也可以。诚然,对于经历了改革开放、下岗潮等等风雨的父母来说,体制内就是幸福和稳定的代名词。他们没有接触过什么叫私募,什么叫PE,什么叫VC,对于父母的观点,兼听则明,因为大多家长一定希望自己孩子稳定,早日成家立业结婚生子,毕竟父母在现在我这个年龄我都已经牙牙学语了,还折腾啥啊。

    对于老师,大多数老师是从来没有去市场上走过一圈的,更没有找过工作。他们对于学生的择业建议多是道听途说而非亲身感悟,所以他们的建议也存在很大的局限性,不得不说市场上不需要教学和科研,更不需要发表论文,所以他们的经验和建议借鉴意义不大。当然如果要读博士找教职另说。建议多找近两年师兄师姐找过工作的人咨询求职选择,更要跟更年长一些的业界人士去沟通人生路径的规划。

     

    2.   处理好课业和实习工作的关系

    楼主属于一直好好学习的那种人,本科好好刷绩点保研金融,硕士好好读paper和老师做助研,一直都是老师眼中的“好学生”,拿了不少奖学金。然而这一切直到我在研一才暑假发现自己就活在象牙塔里。工作单位不会看你的成绩你的发表你的科研,更看你的实习经验。我从研二开始拼命去实习,老师甚至为此还扣了我的奖学金,理由是不好好读paper而去实习。诚然,中国教育存在学术和实践严重的脱节现象。学的东西上班用不了,很多老师的思维也没有转过弯来,通过课程设置等各种方式“防止”学生去实习。在如今实习都要求学生一周四至五天到岗的情况下,学生不得不翘课实习。但是为了工作,必须得这么做。我是很反对大学变成职业培训学校,特别是北大这么好的学术资源和顶尖老师配置,有的学生一头扎进实习,虽然工作找得很好,但未免留下许多青春的遗憾。所以平衡好两者的关系最为重要。

    有效利用好学生身份,多去体验、思考、尝试,找到自己喜欢的、想要的道路,努力去想,拼命去想,越早想明白越好。

     

    3.   眼光一定要放长远,不要因为眼前的利益

    找工作的时候建议大家要想好属于自己的职业发展的道路,要想到这份工作能给下一份工作带来附加值是什么,甚至下下份工作。当然人的目标也是在变化,但是一定要放长远。在工作中到不到价值、与自己的规划不一致到最后只能是“应付工作”。体制内有个神奇的魔力,刚进去的人还有些心气,慢慢的就磨平了棱角,追求越来越少,到最后就是孩子的政策保障学位和处级干部的位置。人也慢慢稳定下来。所以一定要长远规划,如果发现自己不是安于现状的人就一定要早做决定,不能犹豫,因为再过几年,市场上就真的没人要了。当然,我也很反对拿了户口就跑的这种策略,这只会给招生单位留下非常不好的北大精致利己主义者的印象,更会坑掉师弟师妹。圈子很小,在一个地方留下不好的口碑,对你未来发展也是不利的。

     

    三、 一些感悟

    1.  绝大多数人并不知道自己喜欢什么,适合什么,只是社会的一颗螺丝钉在疯狂运转

    上班后才发现工作真的只是一份工作,随着年纪的增长,对生活的热情逐渐消退,到最后大家拼命在工作,结婚,生子,买车买房的程式化道路上。很多人并不知道自己要什么,更不知道什么适合自己。无非就是在社会的光晕下不断裹挟前行。其实贵校绝大部分人也是这样,就普普通通过一辈子,并不知道自己要什么。真正找到自己热爱的事业,努力为之奉献,活着很快乐的人很少很少。

     

    2.  广交朋友,广结善缘

    当我想换工作的时候,我自己投的简历几乎石沉大海。真的很感谢我的同学们,只有靠他们的推荐才能更好地去面试,笔试。在我最痛苦的时候,也是各种同学和好友们不断为我心理疏导,鼓励我,支持我去勇敢离开体制内,去追求自己想要的东西。更有很多从体制内出来的学长学姐跟我说,我的心路历程他们都经历过。披着光鲜的名头,得到邻居和父母同事羡慕的眼光,但却每天做着毫无意义的工作。想出去,发现自己无法接受要重头再来的现实,更无法适应如此激烈的竞争,连自己校招时曾看不上的单位都回不去了,完全想象不出自己曾在高考叱咤风云,成为最顶尖的人进入北大……不过他们都做了,尝试了。不少师兄师姐一直在给我心理上鼓励,让我勇敢走出这一步。

     

    3.   人生路很长,知足常乐

    人生无非四道题目:学业、事业、家庭、生活。大家都已经在第一题上取得了高分,不能奢求每一道题目都是高分,四道题中有一道高分就是人上人了。我们都太渺小,不要什么都想要。现在还有很多人做着”财富自由”的美梦,请放弃这些不切实际的幻想,去做一个幸福的普通人就已经很好了。

     

    无数次的后悔,无数次的埋怨自己当时的选择。11年前的此时我正在高考,盘算着自己一定能上p大。7年前的此时我在本科毕业,和同学们愉快地拍着各种照片。5年前的此时我踌躇满志选择了这个单位,不曾想从此之后每天想到工作的苦衷和被社会大潮抛弃的恐惧都无法入睡。我的缩影其实是内卷的悲哀,让北大清华的学生去做一个中专生就可以做的跑腿快递,去服务领导,去检查文件的错别字和格式,那纳税人的钱真的打了水漂。这是教育的悲哀,当学习沦为配角,疯狂寻找实习为了一份工作,甚至有的顶尖机构实习写着“尽量全勤,不要因论文和学业请假”……这完全脱离了大学教育的本质,但依然学生趋之若鹜。这更是社会的悲哀,体制内机关单位没有把资源配置到最有价值的地方,而是让很多有能力的人埋没于案牍劳形之中,每年还在标榜自己录用了多少个清华北大的学生,为社会舆论展现自己的“高大上”。有的时候想到了初中就会背诵的韩愈的《马说》,“虽有千里之能,食不饱,力不足,才美不外现,且欲与常马等不可得,安求其能千里也?”回头看来,一千年了,问题好像依然存在。


    哀其不幸,怒其不争。我走了很多的弯路,踩了很多的坑,归根结底是缺少职业规划和明确的发展目标,学生思维严重,缺少对于社会的认识。我们缺少了职业教育,缺少了学生到社会的衔接课,我用这么多年来补上。不过不要紧,我才29岁,还有重新再来的资本,对自己说一声没关系,从头再来。

     

    写在最后

    我并不是说体制内一无是处。我也有很多朋友在体制内工作顺利,发展很好。体制内也有其独特的优势。体制外也有很多问题,很多同学都想考公务员。我是想说,没有所谓的“好工作”,更多是要看适合自己的工作。每个人的学科背景、兴趣爱好、性格特点、家庭条件都不同,找工作正是了解自己是匹配自己的过程,祝每个学弟学妹都能找到适合自己的工作。







    Openresty+Lua+Redis灰度发布 - K‘e0llm - 博客园

    $
    0
    0

    灰度发布,简单来说,就是根据各种条件,让一部分用户使用旧版本,另一部分用户使用新版本。百度百科中解释:灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面 来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。上述描述的灰度方案A和B需要等量的服务器,这里我们所做的灰度发布稍作改变:用1-2台机器作为B,B测试成功再部署A。用于WEB系统新代码的测试发布,让一部分(IP)用户访问新版本,一部分用户仍然访问正常版本,原理如图:

    执行过程:
    1、当用户请求到达前端web(代理)服务器Openresty,内嵌的lua模块解析Nginx配置文件中的lua脚本代码;
    2、Lua获取客户端IP地址,去查询Redis中是否有该键值,如果有返回值执行@clien2,否则执行@client1。
    3、Location @client2把请求转发给预发布服务器,location @client1把请求转发给生产服务器,服务器返回结果,整个过程完成。

    Openresty部分配置如下:

    upstream client1 { 
            server127.0.0.1:8080;  #模拟生产服务器
        }
    upstream client2 {
            server127.0.0.1:8090;  #模拟预发布服务器
        }
    
    server {
            listen80;
            server_name  localhost;
            
            location^~ /test {
                content_by_lua_file/app/ngx_openresty/nginx/conf/huidu.lua
            }
            
            location @client1{
                    proxy_pass http://client1;}
            location @client2{
                    proxy_pass http://client2;}
         }

    Lua脚本内容如下:

    local redis = require"resty.redis"local cache=redis.new() 
    cache:set_timeout(60000)
    
    local ok, err= cache.connect(cache,'127.0.0.1',6379)ifnot okthenngx.say("failed to connect:", err) 
        return 
    end 
    
    local red, err= cache:auth("foobared")ifnot redthenngx.say("failed to authenticate:", err)
        return
    end
    
    local local_ip= ngx.req.get_headers()["X-Real-IP"]iflocal_ip == nilthenlocal_ip= ngx.req.get_headers()["x_forwarded_for"]
    endiflocal_ip == nilthenlocal_ip=ngx.var.remote_addr
    end--ngx.say("local_ip is :", local_ip)
    
    local intercept=cache:get(local_ip)ifintercept == local_ipthenngx.exec("@client2")
        return
    end
    
    ngx.exec("@client1")
    
    local ok, err=cache:close()ifnot okthenngx.say("failed to close:", err) 
        return 
    end

    验证:
    url:http://192.168.116.145/test/n.jpg (模拟生产环境)
    客户端IP:192.168.116.1(模拟公司办公网IP)

    1、访问http://192.168.116.145/test/n.jpg
    返回的结果是生产服务器的。

    在Redis存入客户端IP:

    继续访问:
    请求到的是预发布服务器返回的结果。

    在Redis中删除客户端IP:

    然后刷新浏览器:
    返回生产服务器的结果。

    通过lua实现灰度发布

    nginx.conf

    [root@server conf]# cat nginx.conf user root; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; upstream up1{ server 127.0.0.1:8081; } upstream up2{ server 127.0.0.1:8082; } upstream all{ ip_hash; serever 127.0.0.1:8081; serever 127.0.0.1:8082; } server { listen 80; server_name localhost; # 请求交给lua脚本处理 location / { lua_code_cache off; content_by_lua_file /usr/local/nginx/lua/proxy.lua; } # location @ 用于nginx内部跳转 location @up1 { proxy_pass http://up1; } location @up2 { proxy_pass http://up2; } location @all { proxy_pass http://all; } } }

    proxy.lua

    ngx.header.content_type="text/html;charset=utf8" redis = require('resty.redis') redis = redis.new() redis:set_timeout(1000) ok,err = redis:connect('127.0.0.1', 6379) if not ok then ngx.say('connect to redis failed ! reason: ' .. err) end -- 从redis中检查是否存在即将更新的upstream主机 check_up1 = redis:get('update1') --(up1) check_up2 = redis:get('update2') --(up2) redis:close() -- 注意返回的数据类型来判断 if check_up1 == "1" then ngx.exec("@up2") elseif check_up2 == "1" then ngx.exec("@up1") else ngx.exec("@all") end -- 原理就是利用redis中设置指定key,比如要更新主机1,则redis中添加key=update1,value=1,当浏览器请求进入nginx的content阶段后执行lua脚本, -- 脚本中检查redis中是否存在要更新的主机,如果发现某个主机要更新则通过Nginx API for Lua中的ngx.exec接口内部跳转到另一台主机。 -- 如果两个都不更新,则根据nginx自己的方式(默认轮询)分发请求。 -- nginx里使用了ip_hash保持会话,主要是为了用户的请求在后端记录的日志中保持完整。

    Openresty流量复制/AB测试/协程_jinnianshilongnian的专栏-CSDN博客

    $
    0
    0

    流量复制

    在实际开发中经常涉及到项目的升级,而该升级不能简单的上线就完事了,需要验证该升级是否兼容老的上线,因此可能需要并行运行两个项目一段时间进行数据比对和校验,待没问题后再进行上线。这其实就需要进行流量复制,把流量复制到其他服务器上,一种方式是使用如 tcpcopy引流;另外我们还可以使用nginx的HttpLuaModule模块中的ngx.location.capture_multi进行并发执行来模拟复制。

     

    构造两个服务

    location /test1 {
            keepalive_timeout 60s; 
            keepalive_requests 1000;
            content_by_lua '
                ngx.print("test1 : ", ngx.req.get_uri_args()["a"])
                ngx.log(ngx.ERR, "request test1")';
        }
        location /test2 {
            keepalive_timeout 60s; 
            keepalive_requests 1000;
            content_by_lua '
                ngx.print("test2 : ", ngx.req.get_uri_args()["a"])
                ngx.log(ngx.ERR, "request test2")';
        }

      

    通过ngx.location.capture_multi调用

    location /test {
             lua_socket_connect_timeout 3s;
             lua_socket_send_timeout 3s;
             lua_socket_read_timeout 3s;
             lua_socket_pool_size 100;
             lua_socket_keepalive_timeout 60s;
             lua_socket_buffer_size 8k;
    
             content_by_lua '
                 local res1, res2 = ngx.location.capture_multi{
                       { "/test1", { args = ngx.req.get_uri_args() } },
                       { "/test2", { args = ngx.req.get_uri_args()} },
                 }
                 if res1.status == ngx.HTTP_OK then
                     ngx.print(res1.body)
                 end
                 if res2.status ~= ngx.HTTP_OK then
                    --记录错误
                 end
             ';
        }

    此处可以根据需求设置相应的超时时间和长连接连接池等;ngx.location.capture底层通过cosocket实现,而其支持Lua中的协程,通过它可以以同步的方式写非阻塞的代码实现。

     

    此处要考虑记录失败的情况,对失败的数据进行重放还是放弃根据自己业务做处理。

     

    AB测试

    AB测试即多版本测试,有时候我们开发了新版本需要灰度测试,即让一部分人看到新版,一部分人看到老版,然后通过访问数据决定是否切换到新版。比如可以通过根据区域、用户等信息进行切版本。

     

    比如京东商城有一个cookie叫做__jda,该cookie是在用户访问网站时种下的,因此我们可以拿到这个cookie,根据这个cookie进行版本选择。

     

    比如两次清空cookie访问发现第二个数字串是变化的,即我们可以根据第二个数字串进行判断。

    __jda=122270672.1059377902.1425691107.1425691107.1425699059.1

    __jda=122270672.556927616.1425699216.1425699216.1425699216.1。

     

    判断规则可以比较多的选择,比如通过尾号;要切30%的流量到新版,可以通过选择尾号为1,3,5的切到新版,其余的还停留在老版。

     

    1、使用map选择版本 

    map $cookie___jda $ab_key {
        default                                       "0";
        ~^\d+\.\d+(?P<k>(1|3|5))\.                    "1";
    }

    使用 map映射规则,即如果是到新版则等于"1",到老版等于“0”; 然后我们就可以通过ngx.var.ab_key获取到该数据。

    location /abtest1 {
            if ($ab_key = "1") {
                echo_location /test1 ngx.var.args;
            }
            if ($ab_key = "0") {
                echo_location /test2 ngx.var.args;
            }
        }

    此处也可以使用proxy_pass到不同版本的服务器上 

    location /abtest2 {
            if ($ab_key = "1") {
                rewrite ^ /test1 break;
                proxy_pass http://backend1;
            }
            rewrite ^ /test2 break;
            proxy_pass http://backend2;
        }

     

    2、直接在Lua中使用lua-resty-cookie获取该Cookie进行解析

    首先下载lua-resty-cookie

    cd /usr/example/lualib/resty/
    wget https://raw.githubusercontent.com/cloudflare/lua-resty-cookie/master/lib/resty/cookie.lua

     

    location /abtest3 {
            content_by_lua '
    
                 local ck = require("resty.cookie")
                 local cookie = ck:new()
                 local ab_key = "0"
                 local jda = cookie:get("__jda")
                 if jda then
                     local v = ngx.re.match(jda, [[^\d+\.\d+(1|3|5)\.]])
                     if v then
                        ab_key = "1"
                     end
                 end
    
                 if ab_key == "1" then
                     ngx.exec("/test1", ngx.var.args)
                 else
                     ngx.print(ngx.location.capture("/test2", {args = ngx.req.get_uri_args()}).body)
                 end';
    
        }

     首先使用 lua-resty-cookie获取cookie,然后使用ngx.re.match进行规则的匹配,最后使用ngx.exec或者ngx.location.capture进行处理。此处同时使用ngx.exec和ngx.location.capture目的是为了演示,此外没有对ngx.location.capture进行异常处理。

     

    协程

    Lua中没有线程和异步编程编程的概念,对于并发执行提供了协程的概念,个人认为协程是在A运行中发现自己忙则把CPU使用权让出来给B使用,最后A能从中断位置继续执行,本地还是单线程,CPU独占的;因此如果写网络程序需要配合非阻塞I/O来实现。

     

    ngx_lua 模块对协程做了封装,我们可以直接调用ngx.thread API使用,虽然称其为“轻量级线程”,但其本质还是Lua协程。该API必须配合该ngx_lua模块提供的非阻塞I/O API一起使用,比如我们之前使用的ngx.location.capture_multi和lua-resty-redis、lua-resty-mysql等基于cosocket实现的都是支持的。

     

    通过Lua协程我们可以并发的调用多个接口,然后谁先执行成功谁先返回,类似于BigPipe模型。

     

    1、依赖的API 

    location /api1 {
            echo_sleep 3;
            echo api1 : $arg_a;
        }
        location /api2 {
            echo_sleep 3;
            echo api2 : $arg_a;
        }

     我们使用echo_sleep等待3秒。

     

    2、串行实现

    location /serial {
            content_by_lua '
                local t1 = ngx.now()
                local res1 = ngx.location.capture("/api1", {args = ngx.req.get_uri_args()})
                local res2 = ngx.location.capture("/api2", {args = ngx.req.get_uri_args()})
                local t2 = ngx.now()
                ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))';
        }

    即一个个的调用,总的执行时间在6秒以上,比如访问http://192.168.1.2/serial?a=22

    api1 : 22 
    api2 : 22 
    6.0040001869202

     

    3、ngx.location.capture_multi实现

    location /concurrency1 {
            content_by_lua '
                local t1 = ngx.now()
                local res1,res2 = ngx.location.capture_multi({
                      {"/api1", {args = ngx.req.get_uri_args()}},
                      {"/api2", {args = ngx.req.get_uri_args()}}
    
                })
                local t2 = ngx.now()
                ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))';
        }

    直接使用ngx.location.capture_multi来实现,比如访问http://192.168.1.2/concurrency1?a=22

    api1 : 22 
    api2 : 22 
    3.0020000934601

        

    4、协程API实现 

    location /concurrency2 {
            content_by_lua '
                local t1 = ngx.now()
                local function capture(uri, args)
                   return ngx.location.capture(uri, args)
                end
                local thread1 = ngx.thread.spawn(capture, "/api1", {args = ngx.req.get_uri_args()})
                local thread2 = ngx.thread.spawn(capture, "/api2", {args = ngx.req.get_uri_args()})
                local ok1, res1 = ngx.thread.wait(thread1)
                local ok2, res2 = ngx.thread.wait(thread2)
                local t2 = ngx.now()
                ngx.print(res1.body, "<br/>", res2.body, "<br/>", tostring(t2-t1))';
        }

    使用 ngx.thread.spawn创建一个轻量级线程,然后使用 ngx.thread.wait等待该线程的执行成功。比如访问http://192.168.1.2/concurrency2?a=22

    api1 : 22 
    api2 : 22 
    3.0030000209808

       

    其有点类似于Java中的线程池执行模型,但不同于线程池,其每次只执行一个函数,遇到IO等待则让出CPU让下一个执行。我们可以通过下面的方式实现任意一个成功即返回,之前的是等待所有执行成功才返回。

    local  ok, res = ngx.thread.wait(thread1, thread2)

     

    Lua协程参考资料

    《Programming in Lua》

    http://timyang.net/lua/lua-coroutine-vs-java-wait-notify/

    https://github.com/andycai/luaprimer/blob/master/05.md

    http://my.oschina.net/wangxuanyihaha/blog/186401

    http://manual.luaer.cn/2.11.html

     

    100 个网络基础知识普及,看完成半个网络高手!

    $
    0
    0

    1)什么是链接?

    链接是指两个设备之间的连接。它包括用于一个设备能够与另一个设备通信的电缆类型和协议。

    2)OSI 参考模型的层次是什么?

    有 7 个 OSI 层:物理层,数据链路层,网络层,传输层,会话层,表示层和应用层。

    3)什么是骨干网?

    骨干网络是集中的基础设施,旨在将不同的路由和数据分发到各种网络。它还处理带宽管理和各种通道。

    4)什么是 LAN?

    LAN 是局域网的缩写。它是指计算机与位于小物理位置的其他网络设备之间的连接。

    5)什么是节点?

    节点是指连接发生的点。它可以是作为网络一部分的计算机或设备。为了形成网络连接,需要两个或更多个节点。

    6)什么是路由器?

    路由器可以连接两个或更多网段。这些是在其路由表中存储信息的智能网络设备,例如路径,跳数等。有了这个信息,他们就可以确定数据传输的最佳路径。路由器在 OSI 网络层运行。

    7)什么是点对点链接?

    它是指网络上两台计算机之间的直接连接。除了将电缆连接到两台计算机的 NIC卡之外,点对点连接不需要任何其他网络设备。

    8)什么是匿名 FTP?

    匿名 FTP 是授予用户访问公共服务器中的文件的一种方式。允许访问这些服务器中的数据的用户不需要识别自己,而是以匿名访客身份登录。

    9)什么是子网掩码?

    子网掩码与 IP 地址组合,以识别两个部分:扩展网络地址和主机地址。像 IP 地址一样,子网掩码由 32 位组成。

    10)UTP 电缆允许的最大长度是多少?

    UTP 电缆的单段具有 90 到 100 米的允许长度。这种限制可以通过使用中继器和开关来克服

    11)什么是数据封装?

    数据封装是在通过网络传输信息之前将信息分解成更小的可管理块的过程。在这个过程中,源和目标地址与奇偶校验一起附加到标题中。

    12)描述网络拓扑

    网络拓扑是指计算机网络的布局。它显示了设备和电缆的物理布局,以及它们如何连接到彼此。

    13)什么是 VPN?

    VPN 意味着虚拟专用网络,这种技术允许通过网络(如 Internet)创建安全通道。

    例如,VPN 允许您建立到远程服务器的安全拨号连接。

    14)简要描述 NAT

    NAT 是网络地址转换。这是一种协议,为公共网络上的多台计算机提供一种方式来共享到 Internet 的单一连接。

    15)OSI 参考模型下网络层的工作是什么?

    网络层负责数据路由,分组交换和网络拥塞控制。路由器在此层下运行。

    16)网络拓扑如何影响您在建立网络时的决策?

    网络拓扑决定了互连设备必须使用什么媒介。它还作为适用于设置的材料,连接器和终端的基础。

    17)什么是 RIP?

    RIP,路由信息协议的简称由路由器用于将数据从一个网络发送到另一个网络。

    它通过将其路由表广播到网络中的所有其他路由器来有效地管理路由数据。它以跳数为单位确定网络距离。

    18)什么是不同的方式来保护计算机网络?

    有几种方法可以做到这一点。在所有计算机上安装可靠和更新的防病毒程序。确保防火墙的设置和配置正确。用户认证也将有很大的帮助。所有这些组合将构成一个高度安全的网络。

    19)什么是 NIC?

    NIC 是网络接口卡(网卡)的缩写。这是连接到 PC 以连接到网络沈北。每个 NIC都有自己的 MAC 地址,用于标识网络上的 PC。

    20)什么是 WAN?

    WAN 代表广域网。它是地理上分散的计算机和设备的互连。它连接位于不同地区和国家/地区的网络。

    21)OSI 物理层的重要性是什么?

    物理层进行从数据位到电信号的转换,反之亦然。这是网络设备和电缆类型的考虑和设置。

    22)TCP/IP 下有多少层?

    有四层:网络层,互联网层,传输层和应用层。

    23)什么是代理服务器,它们如何保护计算机网络?

    代理服务器主要防止外部用户识别内部网络的 IP 地址。不知道正确的 IP 地址,甚至无法识别网络的物理位置。代理服务器可以使外部用户几乎看不到网络。

    24)OSI 会话层的功能是什么?

    该层为网络上的两个设备提供协议和方法,通过举行会话来相互通信。这包括设置会话,管理会话期间的信息交换以及终止会话时的解除过程。

    25)实施容错系统的重要性是什么?有限吗?

    容错系统确保持续的数据可用性。这是通过消除单点故障来实现的。但是,在某些情况下,这种类型的系统将无法保护数据,例如意外删除。

    26)10Base-T 是什么意思?

    10 是指数据传输速率,在这种情况下是 10Mbps。“Base”是指基带。T 表示双绞线,这是用于该网络的电缆。

    27)什么是私有 IP 地址?

    专用 IP 地址被分配用于内部网。这些地址用于内部网络,不能在外部公共网络上路由。这些确保内部网络之间不存在任何冲突,同时私有 IP 地址的范围同样可重复使用于多个内部网络,因为它们不会“看到”彼此。

    28)什么是 NOS?

    NOS 或网络操作系统是专门的软件,其主要任务是向计算机提供网络连接,以便能够与其他计算机和连接的设备进行通信。

    29)什么是 DoS?

    DoS 或拒绝服务攻击是试图阻止用户访问互联网或任何其他网络服务。这种攻击可能有不同的形式,由一群永久者组成。这样做的一个常见方法是使系统服务器过载,使其无法再处理合法流量,并将被强制重置。

    30)什么是 OSI,它在电脑网络中扮演什么角色?

    OSI(开放系统互连)作为数据通信的参考模型。它由 7 层组成,每层定义了网络设备如何相互连接和通信的特定方面。一层可以处理所使用的物理介质,而另一层则指示如何通过网络实际传输数据。

    31)电缆被屏蔽并具有双绞线的目的是什么?

    其主要目的是防止串扰。串扰是电磁干扰或噪声,可能影响通过电缆传输的数据。

    32)地址共享的优点是什么?

    通过使用地址转换而不是路由,地址共享提供了固有的安全性优势。这是因为互联网上的主机只能看到提供地址转换的计算机上的外部接口的公共 IP 地址,而不是内部网络上的私有 IP 地址。

    33)什么是 MAC 地址?

    MAC 或媒介访问控制,可以唯一地标识网络上的设备。它也被称为物理地址或以太网地址。MAC 地址由 6 个字节组成。

    34)在 OSI 参考模型方面,TCP/IP 应用层的等同层或多层是什么?

    TCP/IP 应用层实际上在 OSI 模型上具有三个对等体:会话层,表示层和应用层。

    35)如何识别给定 IP 地址的 IP 类?

    通过查看任何给定 IP 地址的第一个八位字节,您可以识别它是 A 类,B 类还是 C类。如果第一个八位字节以 0 位开头,则该地址为 Class A.如果以位 10 开头,则该地址为 B 类地址。如果从 110 开始,那么它是 C 类网络。

    36)OSPF 的主要目的是什么?

    OSPF 或开放最短路径优先,是使用路由表确定数据交换的最佳路径的链路状态路由协议。

    37)什么是防火墙?

    防火墙用于保护内部网络免受外部攻击。这些外部威胁可能是黑客谁想要窃取数据或计算机病毒,可以立即消除数据。它还可以防止来自外部网络的其他用户访问专用网络。

    38)描述星形拓扑

    星形拓扑由连接到节点的中央集线器组成。这是最简单的设置和维护之一。

    39)什么是网关?

    网关提供两个或多个网段之间的连接。它通常是运行网关软件并提供翻译服务的计算机。该翻译是允许不同系统在网络上通信的关键。

    40)星型拓扑的缺点是什么?

    星形拓扑的一个主要缺点是,一旦中央集线器或交换机被损坏,整个网络就变得不可用了。

    41)什么是 SLIP?

    SLIP 或串行线路接口协议实际上是在 UNIX 早期开发的旧协议。这是用于远程访问的协议之一。

    42)给出一些私有网络地址的例子。
    • 10.0.0.0,子网掩码为:255.0.0.0
    • 172.16.0.0,子网掩码为:255.240.0.0
    • 192.168.0.0,子网掩码为:255.255.0.0


    43)什么是 tracert?

    Tracert 是一个 Windows 实用程序,可用于跟踪从路由器到目标网络的数据采集的路由。它还显示了在整个传输路由期间采用的跳数。

    44)网络管理员的功能是什么?

    网络管理员有许多责任,可以总结为 3 个关键功能:安装网络,配置网络设置以及网络的维护/故障排除。

    45)描述对等网络的一个缺点。

    当您正在访问由网络上的某个工作站共享的资源时,该工作站的性能会降低。

    46)什么是混合网络?

    混合网络是利用客户端 - 服务器和对等体系结构的网络设置。

    47)什么是 DHCP?

    DHCP 是动态主机配置协议的缩写。其主要任务是自动为网络上的设备分配 IP 地址。它首先检查任何设备尚未占用的下一个可用地址,然后将其分配给网络设备。

    48)ARP 的主要工作是什么?

    ARP 或地址解析协议的主要任务是将已知的 IP 地址映射到 MAC 层地址。

    49)什么是 TCP/IP?

    TCP/IP 是传输控制协议/互联网协议的缩写。这是一组协议层,旨在在不同类型的计算机网络(也称为异构网络)上进行数据交换。

    50)如何使用路由器管理网络?

    路由器内置了控制台,可让您配置不同的设置,如安全和数据记录。您可以为计算机分配限制,例如允许访问的资源,或者可以浏览互联网的某一天的特定时间。

    您甚至可以对整个网络中看不到的网站施加限制。

    51)当您希望在不同平台(如 UNIX 系统和 Windows 服务器之间)传输文件时,可以应用什么协议?

    使用 FTP(文件传输协议)在这些不同的服务器之间进行文件传输。这是可能的,因为 FTP 是平台无关的。

    52)默认网关的使用是什么?

    默认网关提供了本地网络连接到外部网络的方法。用于连接外部网络的默认网关通常是外部路由器端口的地址。

    53)保护网络的一种方法是使用密码。什么可以被认为是好的密码?

    良好的密码不仅由字母组成,还包括字母和数字的组合。结合大小写字母的密码比使用所有大写字母或全部小写字母的密码有利。密码必须不能被黑客很容易猜到,比如日期,姓名,收藏夹等等。

    54)UTP 电缆的正确终止率是多少?

    非屏蔽双绞线网线的正常终止是 100 欧姆。

    55)什么是 netstat?

    Netstat 是一个命令行实用程序。它提供有关连接当前 TCP/IP 设置的有用信息。

    56)C 类网络中的网络 ID 数量是多少?

    对于 C 类网络,可用的网络 ID 位数为 21。可能的网络 ID 数目为 2,提高到 21或 2,097,152。每个网络 ID 的主机 ID 数量为 2,增加到 8 减去 2,或 254。

    57)使用长于规定长度的电缆时会发生什么?

    电缆太长会导致信号丢失。这意味着数据传输和接收将受到影响,因为信号长度下降。

    58)什么常见的软件问题可能导致网络缺陷?

    软件相关问题可以是以下任何一种或其组合:
    • 客户端服务器问题
    • 应用程序冲突
    • 配置错误
    • 协议不匹配
    • 安全问题
    • 用户政策和权利问题


    59)什么是 ICMP?

    ICMP 是 Internet 控制消息协议。它为 TCP/IP 协议栈内的协议提供消息传递和通信。这也是管理由 PING 等网络工具使用的错误信息的协议。

    60)什么是 Ping?

    Ping 是一个实用程序,允许您检查网络上的网络设备之间的连接。您可以使用其 IP 地址或设备名称(如计算机名称)ping 设备。

    61)什么是点对点(P2P)?

    对等是不在服务器上回复的网络。该网络上的所有 PC 都是单独的工作站。

    62)什么是 DNS?

    DNS 是域名系统。该网络服务的主要功能是为 TCP/IP 地址解析提供主机名。

    63)光纤与其他介质有什么优势?

    光纤的一个主要优点是不太容易受到电气干扰。它还支持更高的带宽,意味着可以发送和接收更多的数据。长距离信号降级也非常小。

    64)集线器和交换机有什么区别?

    集线器充当多端口中继器。然而,随着越来越多的设备连接到它,它将无法有效地管理通过它的流量。交换机提供了一个更好的替代方案,可以提高性能,特别是在所有端口上预期有高流量时。

    65)Windows RRAS 服务支持的不同网络协议是什么?

    支持三种主要的网络协议:NetBEUI,TCP/IP 和 IPX。

    66)A,B 和 C 类网络中的最大网络和主机是什么?
    • 对于 A 类,有 126 个可能的网络和 16,777,214 个主机
    • 对于 B 类,有 16,384 个可能的网络和 65,534 个主机
    • 对于 C 类,有 2,097,152 个可能的网络和 254 个主机


    67)直通电缆的标准颜色顺序是什么?

    橙色/白色,橙色,绿色/白色,蓝色,蓝色/白色,绿色,棕色/白色,棕色。

    68)什么协议落在 TCP/IP 协议栈的应用层之下?

    以下是 TCP/IP 应用层协议:FTP,TFTP,Telnet 和 SMTP。

    69)您需要连接两台电脑进行文件共享。是否可以这样做,而不使用集线器或路由器?

    是的,您可以使用一根电缆将两台计算机连接在一起。在这种情况下可以使用交叉型电缆。在这种设置中,一条电缆的数据传输引脚连接到另一条电缆的数据接收引脚,反之亦然。

    70)什么是 ipconfig?

    Ipconfig 是一个常用于识别网络上计算机的地址信息的实用程序。它可以显示物理地址以及 IP 地址。

    71)直通和交叉电缆有什么区别?

    直通电缆用于将计算机连接到交换机,集线器或路由器。交叉电缆用于将两个类似设备连接在一起,如 PC 到 PC 或集线器到集线器。

    72)什么是客户端/服务器?

    客户端/服务器是一种类型的网络,其中一个或多个计算机充当服务器。服务器提供集中的资源库,如打印机和文件。客户端是指访问服务器的工作站。

    73)描述网络。

    网络是指用于数据通信的计算机和外围设备之间的互连。可以使用有线电缆或通过无线链路进行网络连接。

    74)将 NIC 卡从一台 PC 移动到另一台 PC 时,MAC 地址是否也被转移?

    是的,那是因为 MAC 地址是硬连线到 NIC 电路,而不是 PC。这也意味着当 NIC卡被另一个替换时,PC 可以具有不同的 MAC 地址。

    75)解释聚类支持

    群集支持是指网络操作系统在容错组中连接多台服务器的能力。这样做的主要目的是在一台服务器发生故障的情况下,集群中的下一个服务器将继续进行所有处理。

    76)在包含两个服务器和二十个工作站的网络中,安装防病毒程序的最佳位置是哪里?

    必须在所有服务器和工作站上安装防病毒程序,以确保保护。这是因为个人用户可以访问任何工作站,并在插入可移动硬盘驱动器或闪存驱动器时引入计算机病毒。

    77)描述以太网。

    以太网是当今使用的流行网络技术之一。它是在 20 世纪 70 年代初开发的,并且基于 IEEE 中规定的规范。以太网在局域网中使用。

    78)实现环形拓扑有什么缺点?

    如果网络上的一个工作站发生故障,可能会导致整个网络丢失。另一个缺点是,当需要在网络的特定部分进行调整和重新配置时,整个网络也必须被暂时关闭。

    79)CSMA/CD 和 CSMA/CA 有什么区别?

    CSMA/CD 或碰撞检测,每当碰撞发生时重新发送数据帧。CSMA/CA 或碰撞避免,将首先在数据传输之前广播意图发送。

    80)什么是 SMTP?

    SMTP 是简单邮件传输协议的缩写。该协议处理所有内部邮件,并在 TCP/IP 协议栈上提供必要的邮件传递服务。

    81)什么是组播路由?

    组播路由是一种有针对性的广播形式,将消息发送到所选择的用户组,而不是将其发送到子网上的所有用户。

    82)加密在网络上的重要性是什么?

    加密是将信息转换成用户不可读的代码的过程。然后使用秘密密钥或密码将其翻译或解密回其正常可读格式。加密有助于确保中途截获的信息仍然不可读,因为用户必须具有正确的密码或密钥。

    83)如何安排和显示 IP 地址?

    IP 地址显示为一系列由周期或点分隔的四位十进制数字。这种安排的另一个术语是点分十进制格式。一个例子是 192.168.101.2

    84)解释认证的重要性。

    认证是在用户登录网络之前验证用户凭据的过程。它通常使用用户名和密码进行。这提供了限制来自网络上的有害入侵者的访问的安全手段。

    85)隧道模式是什么意思?

    这是一种数据交换模式,其中两个通信计算机本身不使用 IPSec。相反,将 LAN连接到中转网络的网关创建了一个使用 IPSec 协议来保护通过它的所有通信的虚拟隧道。

    86)建立 WAN 链路涉及的不同技术有哪些?

    模拟连接 - 使用常规电话线;数字连接 - 使用数字电话线;交换连接 - 使用发送方和接收方之间的多组链接来移动数据。

    87)网格拓扑的一个优点是什么?

    在一个链接失败的情况下,总会有另一个链接可用。网状拓扑实际上是最容错的网络拓扑之一。

    88)在排除计算机网络问题时,可能会发生什么常见的硬件相关问题?

    大部分网络由硬件组成。这些领域的问题可能包括硬盘故障,NIC 损坏甚至硬件启动。不正确的硬件配置也是其中一个疑难问题。

    89)可以做什么来修复信号衰减问题?

    处理这种问题的常见方法是使用中继器和集线器,因为它将有助于重新生成信号,从而防止信号丢失。检查电缆是否正确终止也是必须的。

    90)动态主机配置协议如何协助网络管理?

    网络管理员不必访问每台客户端计算机来配置静态 IP 地址,而是可以应用动态主机配置协议来创建称为可以动态分配给客户端的范围的 IP 地址池。

    91)解释网络概念的概况?

    配置文件是为每个用户设置的配置设置。例如,可以创建将用户置于组中的配置文件。

    92)什么是 Sneakernet?

    Sneakernet 被认为是最早的联网形式,其中使用可移动介质(如磁盘,磁带)物理传输数据。

    93)IEEE 在计算机网络中的作用是什么?

    IEEE 或电气和电子工程师学会是由电气和电子设备标准发布和管理的工程师组成的组织。这包括网络设备,网络接口,cablings 和连接器。

    94)TCP/IP Internet 层下有哪些协议?

    该层管理的协议有 4 种。这些是 ICMP,IGMP,IP 和 ARP。

    95)谈到网络,什么是权限?

    权限是指在网络上执行特定操作的授权许可。网络上的每个用户可以分配个人权限,具体取决于该用户必须允许的内容。

    96)建立 VLAN 的一个基本要求是什么?

    需要一个 VLAN,因为在交换机级别只有一个广播域,这意味着每当新用户连接时,该信息都会传播到整个网络。交换机上的 VLAN 有助于在交换机级别创建单独的广播域。它用于安全目的。

    97)什么是 IPv6?

    IPv6 或 Internet 协议版本 6 被开发以替代 IPv4。目前,IPv4 正在用于控制互联网流量,但 IPv4 已经饱和。IPv6 能够克服这个限制。

    98)什么是 RSA 算法?

    RSA 是 Rivest-Shamir-Adleman 算法的缩写。它是目前最常用的公钥加密算法。

    99)什么是网格拓扑?

    网格拓扑是一种设置,其中每个设备都直接连接到网络上的每个其他设备。因此,它要求每个设备具有至少两个网络连接。

    100)100Base-FX 网络的最大段长度是多少?

    使用 100Base-FX 的网段的最大允许长度为 412 米。整个网络的最大长度为 5 公里。

    探究实现 Change Request 模式的三种方案

    $
    0
    0
    去年写过一篇文章《浅谈 Pull Request 与 Change Request 研发协作模式》,详细介绍了 PR Flow 以及 CR Flow 的区别,文末有说到 Gitee 应该提供推送分支自动创建评审的改进,刚好近期也听到很多关于平台支持 CR Flow 的声音,而且在不少客户内部也有类似需求,毕竟这个需求可以解放开发者的一部分精力,对于追求研发效能的当下,自然是一个不错的功能点,另外对于 Gerrit 转到 Gitee 的用户,也是一个平滑的过渡方案。所以写下这篇文章,探讨一下支持 CR Flow 平台的产品实现,并对 CR Flow 实现的底层原理做了一些实验性的工作,以供参阅。

    PullRequest & ChangeRequest 分别在说些啥?

    PullRequest (也叫 MergeRequest)最初是由 Github 推出的一种开源协作模式,目前 Gitee、Gitlab等平台都支持这种协作模式,而且这种模式目前更多的用在了团队或者企业的内部代码评审层面,这种协作模式一般是由开发者 Fork 一个仓库,或者在仓库内新起一个分支如 zoker/feature-1,无论是哪种方式,所有的大前提是:开发者没有目标分支的写权限,写权限是被严格控制的,必须经过 PullRequest 方式合入代码。

    在相应的分支研发并自测完成后,通过 Web 界面 提交一个 PullRequest 请求将这些代码合入到目标分支,这个过程会有自动化测试、自动编译构建、代码评审、代码改进、代码合并等一系列操作,最终完成代码向目标分支的合入。

    ChangeRequest 是由 Gerrit 推出的一个概念,Gerrit 是为 AOSP(Android Open Source Project)写的,结合 Repo 工具用来管理庞大的安卓项目,多仓管理也是他的优势之一,但是更多的人把它视为代码评审神器,能够使每一个 Commit 都是可靠。这种模式同样需要严格管控目标分支的写权限,开发者可以在本地起一个分支进行开发。

    与 PullRequest 不同的是,ChangeRequest 不需要开发者到 Web 上进行评审请求的提交,推送一个 Commit 到对应的分支之后,会自动创建一个评审单。而且另外一个比较大的区别就是,ChangeRequest 的评审是针对单 Commit 评审,而 PullRequest 针对的是一个或者多个 Commit 的集中评审。

    详细说明:https://zoker.io/blog/talk-about-pullrequest-and-changerequest

    通过以上的说明,我们大概可以明白了,原来这就是不同协作模式在工具上的区别而已。

    现在人们追求研发效能的提升和改进,说到底就是消除一切可能的浪费,让开发者专注于业务的研发,其他的一些工作能自助化的绝不层层审批严卡,能自动化的绝不浪费开发者时间去自助操作。

    然而 PR Flow 模式就需要开发者在提交完代码之后,手动的前往平台创建 PullRequest,对于很多已经规范化流程的企业来说,这就是一种浪费,因为:

    1. 开发者在接收到卡片的时候已经指定了分支,只要他往这个分支推送代码,那肯定是解决分支对应卡片的问题,无需再进行 PullRequest 的描述的重复填写

    2. 在提交卡片的时候,已经关联好了测试用例,PullRequest 无需再进行测试用例的补充,自动化的从卡片获取用例即可

    3. 已经限制了开发者做提交的时候必须关联一个有效的卡片(如 fixed #JN7HE3),所以开发者在推送代码的时候,这些提交已经有了它们的身份,亦无须再手动创建 PullRequest

    4. ...

    CR Flow 恰巧就解决了这个问题,推送的时候可以自动创建评审,但是采用 PR Flow 的平台如 Github、Gitlab、Gitee 等是没有这个能力的,我做过类似在客户端定制钩子调用接口的实现,但是整体实现上并不顺畅,只有服务端统一处理,才能够考虑到各种情况。

    所以这就是为什么很多做研发协作的 PR Flow 的平台都在补充 CR Flow 能力的原因。

    如何实现推送自动创建评审?

    相信一些了解 Git 的同学第一反应肯定就是 Git Hooks。没错,我在着手去做实验来实现相关逻辑的时候,第一个想到的就是 Update 钩子,但是 Update 钩子还是有一定局限性的。

    不过去年10月份,Git 2.29 增加了一项新的能力 Proc-receive 钩子,这个能力是由阿里巴巴 Codeup 团队蒋鑫老师带领贡献的,有了这个钩子,在服务端能够自定义更新引用的工作,可以想象的空间就比较大了。

    除此之外,还有一种方式就是 Gerrit 方式,我把这种方式称做帽子戏法,也就是自定义了服务端的 RPC 服务,在服务端做完一系列的处理之后,发给客户端我们想让他看到的数据即可。

    所以,要实现推送自动创建评审,目前来看,有三种方式:

    1. Gerrit 的帽子戏法

    2. 自定义 Update 钩子

    3. Git 新能力 Proc-receive 钩子

    下面我们从产品和技术实现上一一展开阐述。

    Gerrit 的帽子戏法

    Gerrit 实现 Change Request 的逻辑是是什么呢?Gerrit 规定了推送到指定的 ref如:

    gitpushorigin HEAD:refs/for/master

    就会进行基于最新的 master进行 commit的检索及评审的创建和更新。

    如果 commit还没有在 master上面,那么就会生成一个新的 Change,对应的 Change 存储在 refs/changes下面,如果没有的话则去 packed-refs找。

    • 其中 refs/changes/67/67/1对应的就是这次提交的 commit

    • refs/changes/67/67/meta则记录此次 Change 的一些基本信息,里面有一个 Change-id 请记住它,这是 Gerrit 能够跟踪具体 commit的关键

    如果 commit已经存在在 master上,并且它被修改了,也就是 commit还是那个 commit,但是通过重写, commit id变了,那就会更新对应的 Change 生成一个新的 Patchset,同样存储在 refs/changes/67/67下,只不过编号是 2

    然而,Gerrit 怎么知道一个重写后的 commit它的前身呢?答案就是上面的 Change-id ,Gerrit 通过用户配置的本地 commit-msg钩子,将 msgcommit id等信息 Hash 成一个 Change-id,这个 Change-id 就是贯穿整个 Change 生命周期的唯一标识符

    那么问题来了,我推送到 refs/for/master的数据,一不用加 -f强推,二我又看不到拿不到 refs/for/master的内容,感觉这个 refs/for/xxxx就是一个 /dev/null,往里面倒腾啥都行,关键是 Git 客户端还没报错,总是提示成功

    * [newbranch]      HEAD -> refs/for/xxx

    这跟我们在用 Github、Gitee 平台的习惯不一样啊。没错,我刚开始接触 Gerrit 的时候,同样有这样的疑惑,猜想 Gerrit 应该是做了一些障眼法,服务端做了一些动作,告诉客户端: 好,你推送的这个分支成功了,你回去吧

    当时去翻阅了 Gerrit 的源码:https://gitee.com/mirrors/Gerrit ,发现 Gerrit 是使用 Java 基于 JGit 自己实现的一套服务端的RPC逻辑,而不是像主流的平台那样使用  receive-pack/upload-pack 进行处理,所以才可以那么灵活的应对。

    // file: java/com/google/gerrit/server/git/receive/ReceiveCommits.java/*** Receives change upload using the Git receive-packprotocol.** <p>Conceptually, mostuseof Gerrit is apushof some commits to refs/for/BRANCH. However, the* receive-packprotocol that this is based on allows multiplerefupdates to be processed at once.* So we have to be prepared to also handle normal pushes (refs/heads/BRANCH),andlegacy pushes* (refs/changes/CHANGE). It is hard tosplitthis class up further, because normal pushes can also* result in updates to reviews, through the autoclose mechanism.*/

    一般情况下,正常往一个分支推送应该是增量的,然后提示我们的信息类似于

    803ed6b..5fd69aaHEAD->master

    而推送到 refs/for/xxx 的动作由 Gerrit 接管,并且在处理接受完数据之后(比如创建和更新 Change),就返回给客户端一个分支已经被创建的假象

    * [newbranch]      HEAD -> refs/for/master

    以此来保证客户端不会报错而疑惑,因为你请求的是更新/创建  refs/for/xxx,如果告诉客户端我创建了其他分支,那客户端就会报错,这受限于 report-status能力,不过现在得益于阿里巴巴的贡献,已经新增了 report-status-v2,这个能力就可以让客户端实现接受服务端实际的变更状态,比如更新一个 Change 后提示

    * [newbranch]      HEAD -> refs/changes/67/67/5

    相信在不久后 Gerrit 应该会跟进。

    The 'report-status-v2' capability extends the protocol byadding new option lines in order to support reporting of reference rewritten by the 'proc-receive' hook. The 'proc-receive' hook may handle a command for a pseudo-reference which may create or update one or more references, and each reference may have different name, different new-oid, and different old-oid.

    那么,该如何自定义  receive-pack 来实现这种帽子戏法呢?

    我基于之前写的一个 http-smart-server(https://gitee.com/kesin/go-git-protocols) 做了一个实验,在调用 receive-pack接收完数据后,不把 receive-pack的输出发给客户端,转而自定义 response数据发给客户端,模拟一个正常推送 masterreceive-pack响应如下:

    pw := pktline.NewEncoder(w)//insertlengthmsg//insertband protocolpw.Encode([]byte("unpack ok\n"))pw.Encode([]byte("ok refs/heads/master\n"))//flushpkt0000

    客户端在接收到这个响应的时候就能知道服务端正确的解压了包并且如期的完成了 refs/heads/master的更新,那我们来搞点不一样的,比如我推送的是 master分支,但是我让服务端返回 master1被更新成功的信息:

    正如上面我们提到的,客户端并不认,因为这这与它的预期不符,转而报错了,我们再次修改代码:

    // callGiteeApi createPr(ref, new_oid, old_oid)// other custom operationspw := pktline.NewEncoder(w)// insert length msg// insert band protocolpw.Encode([]byte("unpack ok\n"))pw.Encode([]byte("ng refs/heads/master Your ref master is sleeping!\n"))// flush pkt 0000

    我们在响应客户端之前,根据现有信息创建了 PullRequest 并且可以根据这次推送的 Context 信息做一些自定义的其他操作,然后把我们期望客户端看到的结果给它:

    通过这种帽子戏法的方式,我们就可以灵活的自定义服务端的处理,来实现我们需要的业务逻辑。

    自定义 Update 钩子

    一般情况下我们在日常使用到 Git 的过程中会涉及到三个服务端的钩子:

    • pre-receive 钩子,用来做一些授权检查工作以及引用的批量更新操作,就算有多个 ref一起更新,也只会执行一次

    • update 钩子,每一个引用的更新都会触发,用来做版本自动化测试之类的工作

    • post-receive 钩子,主要是用来更新引用后的一些工作,比如通知,触发其它服务等

    所以在 Update 钩子做这个事情看起来比较合适,我写了一个 Update 钩子的 Proofs of Concept,用来演示,大概的逻辑:

    1. 推送的时候检查这个分支是否设置了评审分支,如果设置了评审分支,那不允许直接推送,必须走PR

    2. 根据推送的分支和用户进行判定,一个用户在一个评审分支只能有一个PR,类似{username}-{ref}

    3. 自动创建完上述分支后,钩子通过api进行PR的创建

    4. 如果已经存在pr,那么更新即可

    packagemain
    import ("fmt""io""os")
    func main() {iflen(os.Args) !=4{fmt.Println("Need 3 args: <ref> <oldrev> <newrev>")os.Exit(1)}ref:= os.Args[1]oldrev := os.Args[2]newrev := os.Args[3]info := fmt.Sprintf("ref: %s, old: %s, new: %s",ref, oldrev, newrev)fmt.Println(info)
    ifref=="refs/heads/review"{reviewRefPath := fmt.Sprintf("%s/%s-zoker", os.Getenv("GIT_DIR"),ref)reviewRefExist := truerefFile, err := os.Open(reviewRefPath)iferr != nil {reviewRefExist = falserefFile, err = os.Create(reviewRefPath)}// check non fast-forward between reviewRefandnewrevandthenexit1ifneededio.WriteString(refFile, newrev) //ifcommit is fast forward// involve pr api to create prandalert// createOrUpdateSucess := remote call resultifreviewRefExist {fmt.Printf("Pushes have successfully update in PullRequest: https://gitee.com/xxx/xxx/pulls/xx \n")}else{fmt.Printf("You don't have permission push to %s \n",ref)fmt.Printf("We generate a new branch %s-zoker and automaticlly create a \n",ref)fmt.Printf("PullRequest for you: https://gitee.com/xxx/xxx/pulls/xx\n")}os.Exit(1)}}


    通过对 review分支的检测,来做对应的处理:

    如果当前用户在对应的目标分支还没有提交 PullRequest,那就创建并提示:

    如果当前用户在对应的目标分支已经提交了 PullRequest,那就更新:

    上面默认是不冲突的情况,如果冲突的话,我们需要:

    1. 如果 review-zoker检测到与 newrev冲突,可以不给更新

    2. 如果 review分支冲突的话,是无法继续推送的,这其实不太不合理,因为实际是要更新 review-zoker

    而且提示中夹在着错误信息,会对用户产生困扰,我在研究 ezOne 这个产品的时候,发现他们的做法应该是是围绕着 Update 钩子进行的,因为实际试用的情况与我的实验脚本结果是一样的:

    只不过 ezOne 把这些提示信息加了颜色,着重展示给用户,但是使用 Update 钩子实现所产生冲突的问题并没有解决

    此外 ezOne 应该也在返回给客户的响应上做了一些处理,或者结合使用了上面说的 proc-receive钩子,因为拒绝 master的推送并给出了提示 代码评审通过后方可合入。整体来讲,ezOne 对于这块功能起了一个新的名字叫 DCR(Direct Change Request)。

    ezOne DCR 整体使用逻辑总结如下:

    1. 分支只要是保护分支,那么推送就会被拒绝,转而自动产生一个 DCR

    2. 一个用户对一个目标分支可以有多个 DCR,如果 pushed..master包含某个 pr..master,那就更新,如果不包含那就新建

    3. 会严格检查push与目标分支的冲突情况,如果强推也完全按照规则1进行的

    4. 更新也只更新线性向前的 DCR,其他的情况并不会更新

    5. 如果 Base 和目标分支冲突了,就直接冲突拒绝推送,没有找到此种情况更新 DCR 的方式

    Git 新能力 Proc-receive 钩子

    上面对 proc-receive钩子有过一些简单的介绍,归根结底就是 receive-pack在更新引用的时候会把匹配到 receive.procReceiveRefs的引用转交给新的钩子 proc-receive进行处理,这个 proc-receive钩子与 receive-pack之间通过 PKT-LINE 格式的命令进行通讯。

    两者之间的通信大体如下(我做了详细的注解,你应该可以看得明白):

    # receive-pack 与 proc-receive 相互协商版本和能力S: PKT-LINE(version=1\0push-options atomic...)S:flush-pktH: PKT-LINE(version=1\0push-options...)H:flush-pkt
    # receive-pack 在匹配到 receive.procReceiveRefs 的引用后,把这些引用的更新操作转交给 proc-receive 钩子进行处理S: PKT-LINE(<old-oid> <new-oid> <ref>)S: ... ...S:flush-pkt
    # pro-receive 钩子接收到这些信息,根据自己的逻辑进行对应的处理,新建引用,更新或者创建 PullRequest 可以安插到这里进行# 如果 proc-receive 钩子处理无误,则告诉 receive-pack 已经处理成功H: PKT-LINE(ok <ref>)
    # 如果 proc-receive 钩子处理失败,亦或创建或更新 PullRequest 失败,均可以自定义这些引用更新失败的信息H: PKT-LINE(ng <ref> <reason>)
    # 如果发现自己没法处理,甩锅给 receive-pack 继续处理即可H: PKT-LINE(ok <ref>)H: PKT-LINE(optionfall-through)

    如果你对 proc-receive 钩子实现的原理感兴趣,可以参见蒋老师的演讲文稿: https://git-repo.info/zh_cn/2020/03/agit-flow-and-git-repo/

    基于上述对  proc-receive 钩子的理解,我使用 Go 简单的写了一个  proc-receive 钩子,代码如下:

    packagemain
    import("os")
    funcmain(){// init pkt reader and writerpd := NewPktDecoder(os.Stdin)pe := NewPktEncoder(os.Stdout)
    // get receive-pack version_ = pd.Decode(&[]byte{})_ = pd.Decode(&[]byte{})
    // send proc-receive version_ = pe.Encode([]byte("version=1"))_ = pe.Encode(nil)
    // scan pktline for matched receive.procReceiveRefsvarc []byte_ = pd.Decode(&c)// c -> command a b refs/xxx/xxx_ = pd.Decode(&[]byte{})
    // ----------------------// do what you want, eg: generate ref, call api to create a pr on gitee...// newRef("refs/heads/zoker/xxx", ZERO_OID, c.new_oid)// createGiteePullRequest(new_ref, target_ref)// ----------------------
    //_ = pe.Encode([]byte("ok refs/heads/review"))_ = pe.Encode([]byte("ng refs/heads/review PullRequest created or updated, please........"))_ = pe.Encode(nil)}

    主要的功能就是与 receive-pack进行通讯,然后告诉 receive-pack分支 review是否更新成功,我们可以在这里进行对应的逻辑处理,比如基于这次推送新建一个引用,或者为每个 Commit 都新建一个引用,基于这个引用去创建一个评审等

    阿里云的 Codeup 也就是基于  proc-receive 能力实现的 ChangeRequest 功能,不过整体的产品逻辑是参照 Gerrit 的,与 Gerrit 一样支持往  refs/for/master 推送生成 PullRequest 的,于此同时也支持  refs/drafts/refs/for-review/等特殊的前缀,主要是在合入权限上的区别。

    Codeup 的 CR 功能使用总结如下:

    1. 同一个用户对一个目标分支只能创建一个CR

    2. 推送到  refs/for/master 符合 Gerrit 的工作方式,不会报分支未更新的错

    3. 更新、回退提交均可更新 CR,回退到目标分支之后,CR 提交就空了

    4. 与源冲突的情况可以直接推送,不需要 -f,直接覆盖原 CR

    5. 严格按照  HEAD 与  master 的差异来更新 CR,不管是否冲突

    6. 与目标分支冲突的情况可以直接推送,但是PR打不开了,一直转圈圈,有的关闭重开就好了,但有的关闭重开也不行

    7. 如果有两个以上提交,CR 标题为  merge from xxxxxx,如果只有一个,标题就直接使用了这个提交的提交信息

    此外,在使用腾讯工蜂的代码评审功能中,觉得工蜂的设计挺有意思,而且具体的实现也应该是使用了帽子戏法或者 proc-receive钩子,工蜂提供了两个概念:合并请求、代码评审

    • 合并请求:与常规的 PullRequest 功能逻辑没太大区别

    • 代码评审:可以创建某一个或者多个提交的评审单,用于做代码评审

    工蜂对分支提供了两种设置,一种是推送自动创建评审单,也就是无论你往目标分支上推送了什么,都会创建一个评审单用于代码评审,推送会成功更新到目标分支,不会造成中断;另外一种是推送自动创建评审单,但是会拒绝更新目标分支,需要通过评审才能继续推送新的提交。

    工蜂的 CR 功能使用总结如下:

    1. 保护分支不可强推,不可推送包含多个父提交的提交(可能我哪里没设置对)

    2. 自动创建评审有两种选项,推送自动创建评审(仓库级),推送自动创建评审并且通过才能合并(保护分支)

    3. 按照单次推送的行为来的(你推上来了什么,就用什么创建 CR,强推也一样,不存在更新的说法)

    4. 保护分支的严格模式,推送自动创建了合并请求,看提示应该是修改了协议返回,自动创建了  refs/for/zoker/master/1,用时间戳作为合并请求的标题

    5. 已经有合并请求的情况下,无法再次推送 master,会提示  remote: Code review must be approved before push

    6. 没找到更新这个自动创建的合并请求的方式(姿势不对还是设定如此?)

    7. 关于冲突情况,如果跟目标分支冲突,直接提示冲突,拉取合并再去推送则提示:You arenotallowedtopush codeto a protectedbranchonthisproject,删除 CR 也一样,移除保护分支也一样,尬住了,但是回退后重新产生新的冲突提交即可推送,然后强推到之前的版本,再把刚刚不能推送的合并再次推送,就可以了,估计是什么缓存逻辑被刷新了

    谈谈 Gitee 轻量级 Pull Request 的改进

    Gitee 在去年推出了轻量级 PR 功能(https://gitee.com/help/articles/4291),旨在解决用户为开源项目提交贡献过程繁琐的问题。

    从其本质上来看,轻量级 PR 要解决的问题其实与现在 Change Request 要解决的问题是一样的,都是为了减少用户的使用成本,使其专注代码层面的贡献。

    轻量级 PR 功能目前只是在网页通过 WebIDE 进行提交,其实通过上面我们提到的一些技术,可以做到对于开源项目,直接 Clone 到本地进行修改,然后推送到目标仓库的某个分支即可,服务端可以针对推送进行验证,如果推送的为开源项目并且用户无授权,那么就自动生成或者更新 Pull Request 即可。

    总结

    目前,各个产品都在找寻一种合适的方式来覆盖用户对于自动创建评审的需求。但是我有一种很明显的感觉,就是每一个产品的实现逻辑,都包含了强烈的自身组织的研发协作方式,每种实现逻辑都可以覆盖一部分用户需求,但是并未能满足绝大多数,可能这也跟形形色色的研发协作模式有关。有时候想想,与其绞尽脑汁去想着如何在产品上满足不同用户的需求,倒不如自己定义一个标准,让用户来遵守和使用。

    此外,对于 Change Request 功能的技术实现, proc-receive钩子的出现无疑降低了技术门槛,避免了对 receive-pack业务逻辑的侵入。但无论使用哪种方式最终还是要从用户实际需求出发,抓住用户真正的痛点,才能通过技术真真正正的解决问题,而不是又创造了新的问题。

    最后,要感谢我的女朋友在进行这篇文章撰写时给我的鼓励和帮助,没有她的支持,我早就写完了。

    对于 Change Request 这种协作模式,你有什么建议吗?

    浅析开源项目之Ceph - 知乎

    $
    0
    0

    前言

    Ceph是一个极其复杂的统一分布式存储系统,运维操作门槛高、稳定性不错,性能差强人意,虽然各大厂都在自研分布式存储,但Ceph是不可或缺的参考对象。本文参考了Ceph源代码以及网上各路大神文章,如有侵权,联系删除。简要分析Ceph的架构、重要的模块以及基于Seastar的未来规划,使读者对Ceph有一个大致清晰的认识。

    目录

    • 1 Ceph概述
    • 2 核心组件
    • 3 IO流程
    • 4 IO顺序性
    • 5 PG一致性协议
      • 5.1 StateMachine
      • 5.2 Failover Overview
      • 5.3 PG Peering
      • 5.4 Recovery/Backfill
    • 6 引擎概述
    • 7 FileStore
      • 7.1 架构设计
      • 7.2 对外接口
      • 7.3 日志类型
      • 7.4 幂等操作
    • 8 BlueStore
      • 8.1 架构设计
      • 8.2 BlockDevice
      • 8.3 磁盘分配器
      • 8.4 BlueFS
      • 8.5 对象IO
    • 9 未来规划

    1 Ceph概述

    Ceph是由学术界(Sage Weil博士论文)在2006年提出的一个开源的分布式存储系统的解决方案,最早致力于下一代高性能分布式文件存储,经过十多年的发展,还提供了块设备、对象存储S3的接口,成为了统一的分布式存储平台,进而成为开源社区存储领域的明星项目,得到了广泛的实际应用。

    Ceph是一个可靠的、自治的、可扩展的分布式存储系统,它支持文件存储、块存储、对象存储三种不同类型的存储,满足存储的多样性需求。整体架构如下:

    • 接口层:提供客户端访问存储层的的各种接口,支持POSIX文件接口、块设备接口、对象S3接口,以及用户可以自定义自己的接口。
    • Librados:提供上层访问RADOS集群的各种库函数接口,libcephfs、librbd、librgw都是Librados的客户端。
    • RADOS:可靠的、自治的分布式对象存储,主要包含Monitor、OSD、MDS节点,提供了一个统一的底层分布式存储系统,支持逻辑存储池概念、副本存储和纠删码、自动恢复、自动rebalance、数据一致性校验、分级缓存、基于dmClock的QoS等核心功能。

    2 核心组件

    • CephFS:Ceph File System,Ceph对外提供的文件系统服务,MDS来保存CephFS的元数据信息,数据写入Rados集群。
    • RBD:Rados Block Device,Ceph对外提供的块设备服务,Ceph里称为Image,元数据很少,保存在特定的Rados对象和扩展属性中,数据写入Rados集群。
    • RGW:Rados Gateway,Ceph对外提供的对象存储服务,支持S3、Swift协议,元数据保存在特定的Pool里面,数据写入Rados集群。
    • Monitor:保存了MONMap、OSDMap、CRUSHMap、MDSMap等各种Map等集群元数据信息。一个Ceph集群通常需要3个Mon节点,通过Paxos协议同步集群元数据。
    • OSD:Object Storage Device,负责处理客户端读写请求的守护进程。一个Ceph集群包含多个OSD节点,每块磁盘一个OSD进程,通过基于PGLog的一致性协议来同步数据。
    • MDS:Ceph Metadata Server,文件存储的元数据管理进程,CephFS依赖的元数据服务,对外提供POSIX文件接口,不是Rados集群必须的。
    • MGR:Ceph Manager,负责跟踪运行时指标以及集群的运行状态,减轻Mon负担,不是Rados集群必须的。
    • Message:网络模块,目前支持Epoll、DPDK(剥离了seastar的网络模块,不使用其share-nothing的框架)、RDMA,默认Epoll。
    • ObjectStore:存储引擎,目前支持FileStore、BlueStore、KVStore、MemStore,提供类POSIX接口、支持事务,默认BlueStore。
    • CRUSH:数据分布算法,秉承着无需查表,算算就好的理念,极大的减轻了元数据负担(但是感觉过于执着减少元数据了,参考意义并不是很大),但同时数据分布不均,不过已有 CRUSH优化Paper
    • SCRUB:一致性检查机制,提供scrub(只扫描元数据)、deep_scrub(元数据和数据都扫描)两种方式。
    • Pool:抽象的存储池,可以配置不同的故障域也即CRUSH规则,包含多个PG,目前类型支持副本池和纠删池。
    • PG:Placement Group,对象的集合,可以更好的分配和管理数据,同一个PG的读写是串行的,一个OSD上一般承载200个PG,目前类型支持副本PG和纠删PG。
    • PGLog:PG对应的多个OSD通过基于PGLog的一致性协议来同步数据,仅保存部分操作的oplog,扩缩容、宕机引起的数据迁移过程无需Mon干预,通过PG的Peering、Recovery、Backfill机制来自动处理。
    • Object:Ceph-Rados存储集群的基本单元,类似文件系统的文件,包含元数据和数据,支持条带化、稀疏写、随机读写等和文件系统文件差不多的功能,默认4MB。

    3 IO流程

    此处以RBD块设备为例简要介绍Ceph的IO流程。

    1. 用户创建一个Pool,并指定PG的数量。
    2. 创建Pool/Image,挂载RBD设备,映射成一块磁盘。
    3. 用户写磁盘,将转换为对librbd的调用。
    4. librbd对用户写入的数据进行切块并调用librados,每个块是一个object,默认4MB。
    5. librados进行 stable_hash算法计算object所属的PG,然后再输入pg_id和CRUSHMap,根据CRUSH算法计算出PG归属的OSD集合。
    6. librados将object异步发送到Primary PG,Primary PG将请求发送到Secondary PG。
    7. PG所属的OSD在接收到对应的IO请求之后,调用ObjectStore存储引擎层提供的接口进行IO。
    8. 最终所有副本都写入完成才返回成功。

    Ceph的IO通常都是异步的,所以往往伴随着各种回调,以FileStore为例看下ObjectStore层面的回调:

    1. on_journal:数据写入到journal,通常通过DirectIO + Libaio的方式,Journal的数据是sync到磁盘上的。
    2. on_readable:数据写入Journal且写入Pagecache中,返回客户端可读。
    3. on_commit:Pagecache中的数据sync到磁盘上,返回客户端真正写成功。

    4 IO顺序性

    分布式系统中通常需要考虑对象读写的顺序性和并发性,如果两个对象没有共享资源,那么就可以并发访问,如果有共享资源就需要加锁操作。对于同一个对象的并发读写来说,通常是通过队列、锁、版本控制等机制来进行并发控制,以免数据错乱,Ceph中对象的并发读写也是通过队列和锁机制来保证的。

    PG

    Ceph引入PG逻辑概念来对对象进行分组,不同PG之间的对象是可以并发读写的,单个PG之间的对象不能并发读写,也即理论上PG越多并发的对象也越多,但对于系统的负载也高。

    不同对象的并发控制

    落在不同PG的不同对象是可以并发读写的,落在统一PG的不同对象,在OSD处理线程中会对PG加锁,放进PG队列里,一直等到调用queue_transactions把OSD的事务提交到ObjectStore层才释放PG的锁,也即

    对于同一个PG里的不同对象,是通过PG锁来进行并发控制,不过这个过程中不会涉及到对象的IO,所以不太会影响效率。

    同一对象的并发控制

    同一对象的并发控制是通过PG锁实现的,但是在使用场景上要分为单客户端、多客户端。

    1. 单客户端:单客户端对同一个对象的更新操作是串行的,客户端发送更新请求的顺序和服务端收到请求的顺序是一致的。
    2. 多客户端:多客户端对同一个对象的并发访问类似于NFS的场景,RADOS以及RBD是不能保证的,CephFS理论上应该可以。

    所以接下来主要讨论单客户端下同一对象的异步并发更新。

    Message层顺序性

    1. TCP层是通过消息序列号来保证一条连接上消息的顺序性。
    2. Ceph Message层也是通过全局唯一的tid来保证消息的顺序性。

    PG层顺序性

    从Message层取到消息进行处理时,OSD处理OP时划分了多个shard,每个shard可以配置多个线程,PG通过哈希的方式映射到不同的shard里面。OSD在处理PG时,从拿到消息就会PG加了写锁,放入到PG的OpSequencer队列,等到把OP请求下发到ObjectStore端才释放写锁。对于同一个对象的并发读写通过对象锁来控制。

    对同一个对象进行写操作会加write_lock,对同一个对象的读操作会加read_lock,也就是读写锁,读写是互斥的。写锁从queue_transactions开始到数据写入到Pagecache结束。

    对同一个对象上的并发写操作,实际上并不会发生,因为放入PG队列是有序的,第一次写从PG取出放到ObjectStore层之后就会释放锁,然后再把第二次写从PG取出放入到ObjectStore层,取出写OP放到ObjectStore层都是调的异步写的接口,这就需要ObjectStore层来保证两次写的顺序性了。

    ObjectStore层顺序性

    ObjectStore支持FileStore、BlueStore,也都需要保证IO顺序性。对于写请求,到达ObjectStore层之后,会获取OpSequencer(每个PG一个,用来保证PG内OP顺序)。

    FileStore:对于写事务OP来说(都有一个唯一递增的seq),会按照顺序放进writeq队列,然后write_thread线程通过Libaio将数据写入到Journal里面,此时数据已经是on_disk但不可读,已完成OP的seq序号按序放到journal的finisher队列里(因为Libaio并不保证顺序,会出现先提交的IO后完成,因此采用op的seq序号来保证完成后处理的顺序),如果某个op之前的op还未完成,那么这个op会等到它之前的op都完成后才一起放到finisher队列里,然后把数据写入到Pagecache和sync到数据盘上。

    BlueStore:bluestore在拿到写OP时会先通过BlockDevice提供的异步写(Libaio/SPDK/io_uring)接口先把数据写到数据盘,然后再通过RocksDB的WriteBatch接口批量的写元数据和磁盘分配器信息到RocksDB。由于也是通过异步写接口写的,也需要等待该OP之前的OP都完成,才能写元数据到RocksDB。

    5 PG一致性协议

    在Ceph的设计和实现中,自动数据迁移、自动数据均衡等各种特性都是以PG为基础实现的,PG是最复杂和最难理解的概念,Ceph也基于PG实现了数据的多副本和纠删码存储。基于PG LOG的一致性协议也类似于Raft实现了强一致性。

    5.1 StateMachine

    PG有20多种状态,状态的多样性也反映了功能的多样性和复杂性。PG状态的变化通过事件驱动的状态机来驱动,比如集群状态的变化,OSD加入、删除、宕机、恢复 、创建Pool等,最终都会转换为一系列的状态机事件,从而驱动状态机在不同状态之间跳转和执行处理。

    • Active:活跃态,PG可以正常处理来自客户端的读写请求,PG正常的状态应该是Active+Clean的。
    • Unactive:非活跃态,PG不能处理读写请求。
    • Clean:干净态,PG当前不存在修复对象,Acting Set和Up Set内容一致,并且大小等于存储池的副本数。
    • Peering:类似Raft的Leader选举,使一个PG内的OSD达成一致,不涉及数据迁移等操作。
    • Recovering:正在恢复态,集群正在执行迁移或恢复某些对象的副本。
    • Backfilling:正在后台填充态,backfill是recovery的一种特殊场景,指peering完成后,如果基于当前权威日志无法对Peers内的OSD实施增量同步(OSD离线太久,新的OSD加入) ,则通过完全拷贝当前Primary所有对象的方式进行全量同步。
    • Degraded:降级状态,Peering完成后,PG检测到有OSD有需要被同步或修复的对象,或者当前ActingSet 小于存储池副本数。
    • Undersized:PG当前Acting Set小于存储池副本数。ceph默认3副本,min_size参数通常为2,即副本数>=2时就可以进行IO,否则阻塞IO。
    • Scrubing:PG正在进行对象的一致性扫描。
    • 只有Active状态的PG才能进行IO,可能会有active+clean(最佳)、active+unclean(小毛病)、active+degraded(小毛病)等状态,小毛病不影响IO。
    为了避免全是文字,网上找了张图,如有侵权,联系删除。

    5.2 Failover Overview

    故障检测:Ceph分为MON集群和OSD集群两部分,MON集群管理者整个集群的成员状态,将OSD的信息存放在OSDMap中,OSD定期向MON和Peer OSD 发送心跳包,声明自己处于在线状态。MON接收来自OSD的心跳信息确认OSD在线,同时也接收来自OSD对于Peer OSD的故障检测。当MON判断某个OSD节点离线后,便将最新的OSDMap通过心跳随机的发送给OSD,当Client或者OSD处理IO请求时发现自身的OSDMap版本低于对方,便会向MON请求最新的OSDMap,这种Lasy的更新方式,经过一段时间的传播之后,整个集群都会收到最新的OSDMap。

    确定恢复数据:OSD在收到OSDMap的更新消息后,会扫描该OSD下所有的PG,如果发现某些PG已经不属于自己,则会删掉其数据。如果该OSD上的PG是Primary PG的话,将会进行PG Peering操作。在Peering过程中,会根据PGLog检查多个副本的一致性,并计算PG的不同副本的数据缺失情况,PG对应的副本OSD都会得到一份对象缺失列表,然后进行后续的Recovery,如果是新节点加入、不足以根据PGLog来Recovery等情况,则会进行Backfill,来恢复整份数据。

    数据恢复:在PG Peering过程中会暂停所有的IO,等Peering完成后,PG会进入Active状态,此时便可以接收数据的IO请求,然后根据Peering的信息来决定进行Recovery还是Backfill。对于Replica PG缺失的数据Primary PG会通过Push来推送,对于Primary PG自身缺少的数据会通过Pull方式从其他Replicate PG拉取。在Recovery过程中,恢复的粒度是4M对象,对于无法通过PGlog来恢复的,则进行Backfill进行数据的全量拷贝,等到数据恢复完成后,PG的状态会标记为Clean即所有副本数据保持一致。

    5.3 PG Peering

    PG的Peering是使一个PG内的所有OSD达成一致的过程,相关重要概念如下:

    • up set:pg对应的副本列表,也即通过CRUSH算法选出来的3个副本列表,第一个为primary,其他的为replica。
    • active set:对外处理IO的副本列表,通常和up set一致,当恢复时可能会存在临时PG,则active set为临时PG的副本集合,用于对外提供正常IO,当完成恢复后,active set调整为up set。
    • pg_temp:临时的PG,当CRUSH算法产生新的up set的primary无法承担起职责(新加入的OSD或者PGLog过于落后的OSD成为了primary,也即需要backfill的primary需要申请临时PG,recovery的primary不需要申请临时PG),osd就会向mon申请一个临时的PG用于数据正常IO和恢复,Ceph做了优化是在进行CRUSH时就根据集群信息选择是否预填充pg_tmp,从而减少Peering的时间。此时处于Remapped状态,等到数据同步完成,需要取消pg_tmp,再次通过Peering将active_set切回up_set。
    • epoch:每个OSDMap都会有一个递增的版本,值越大版本越新,当集群中OSD发生变化时,就会产生新的OSDMap。
    • pg log:保存操作的记录,是用于数据恢复的重要结构。并不会保存所有的op log,默认3000条,当有数据需要恢复的时候就会保存10000条。
    • Interval:每个PG都有Interval(epoch的操作序列),每次OSD获取到新的OSDMap时,如果发现 up set、up primary、active set、active primary没有改变,则Interval不用改变,否则就要生成新的current interval,之前的变成past_interval,只要该PG内部的OSD不发生变化,Interval就不会变化。

    主要包含三个步骤:

    1. GetInfo:作用为确定参与peering过程的osd集合。主OSD会获取该PG对应的所有OSD的pg_info信息放入peer_info。
    2. GetLog:作用为选取权威日志。根据各个副本OSD的pg_info信息比较,选取一个具有权威日志的OSD,如果主OSD不具备权威日志,那么就从该具有权威日志的OSD拉取权威日志,拉取完成之后进行合并就具有了权威日志,如果primary自身具有权威日志,则不用合并,否则合并的过程如下:
      1. 拉取过来的日志比primary具有更老的日志条目:追加到primary本地日志尾部即可。
      2. 拉取过来的日志比primary具有更新的日志条目:追加到primary本地日志头部即可。
      3. 合并的过程中,primary如果发现自己有对象需要修复,便会将其加入到missing列表。
    3. GetMissing:获取需要恢复的object集合。主OSD拉取其他从OSD的PGLog,与自身权威日志进行对比,计算该OSD缺失的object集合。

    5.4 Recovery/Backfill

    Peering进行之后,如果Primary检测到自身或者任意一个Peer需要修复对象,则进入Recovery状态,为了影响外部IO,也会限制恢复的速度以及每个OSD上能够同时恢复的PG数量。Recovery一共有两种状态:

    1. Pull:如果Primary自身存在待恢复对象,则按照missing列表寻找合适的副本拉取修复对象到本地然后修复。
    2. Push:如果Primary检测到其Replica存在待恢复对象,则主动推动待修复对象到Replica,然后由Replica自身修复。

    通常总是先执行Pull再执行Push,即先修复Primary再修复Replica,因为Primary承担了客户端的读写,需要优先进行修复,修复情况大致如下:

    1. 客户端IO和内部恢复IO可以同时进行。
    2. 读写的对象不在恢复列表中:按照正常IO即可。
    3. 读取的对象在恢复列表中:如果primary有则可以直接读取,如果没有需要优先恢复该对象,然后读取。
    4. 写入的对象在恢复列表中:优先恢复该对象,然后写入。
    5. backfill则是primary遍历当前所有的对象,将他们全量拷贝到backfill 的PG中。
    6. 恢复完成后,会重新进行Peering,是active set 和up set保持一致,变为active + clean状态。

    在恢复对象时,由于PGLog并未记录关于对象修改的详细信息(offset、length等),所以目前对象的修复都是全量对象(4M)拷贝,不过社区已经支持 部分对象修复

    同时在恢复对象时,由于ObjectStore支持覆盖写,所以在对象上新的写不能丢弃老的对象,需要等老的对象恢复完之后,才能进行该对象新的写入,不过社区已经支持 异步恢复

    6 引擎概述

    Ceph提供存储功能的核心组件是RADOS集群,最终都是以对象存储的形式对外提供服务。但在底层的内部实现中,Ceph的后端存储引擎在近十年来经历了许多变化。现如今的Ceph系统中仍然提供的后端存储引擎有FileStore、BlueStore。但该三种存储引擎都是近年来才提出并设计实现的。Ceph的存储引擎也先后经历了EBOFS-->FileStore/btrfs-->FileStore/xfs-->NewStore-->BlueStore。同时Ceph需要支持文件存储,所以其存储引擎提供的接口是类POSIX的,存储引擎操作的对象也具有类似文件系统的语义,也具有其自己的元数据。

    7 FileStore

    FileStore是Ceph基于文件系统的最早在生成环境比较稳定的单机存储引擎,虽然后来出现了BlueStore,但在一些场景中仍然不能代替FileStore,比如在全是HDD的场景中FileStore可以使用NVME盘做元数据和数据的读写Cache,从而加速IO,BlueStore就只能加速元数据IO。

    7.1 架构设计

    FileStore是基于文件系统的,为了维护数据的一致性,写入之前数据会先写Journal,然后再写到文件系统,会有一倍的写放大。不过Journal也起到了随机写转换为顺序写、支持事务的作用。

    引用网上图片,如有侵权,联系删除。

    7.2 对外接口

    对象的元数据使用KV形式保存,主要有两种保存方式:

    • xattrs:保存在本地文件系统的扩展属性中,一般都有大小的限制。
    • omap:object map,保存在LevelDB/RocksDB中。

    有些文件系统不支持扩展属性,或者扩展属性大小有限制。一般情况下xattr保存一些比较小且经常访问的元数据,omap保存一些大的不经常访问的元数据。

    同时ObjectStore使用Transaction类来实现相关的操作,将元数据和数据封装到bufferlist里面,然后写Journal。大致包含OP_TOUCH、OP_WRITE、OP_ZERO、OP_CLONE等42种 事务操作。提供的对外接口大致有:

    ObjectStore本身的接口:mount、umount、fsck、repair、mkfs等。

    Object本身的接口:read、write、omap、xattrs、snapshot等。

    7.3 日志类型

    在FileStore的实现中,根据不同的日志提交方式,有两种不同的日志类型:

    • Journal writeahead:先提交数据到Journal上(通常配置成一块SSD磁盘),然后再写入到Pagecache,最后sync到数据盘上。适用于XFS、EXT4等不支持快照的文件系统,是FileStore默认的实现方式。
    • Journal parallel:数据提交到Journal和sync到数据盘并行进行,没有完成的先后顺序,适用于BTRFS、ZFS等支持快照的文件系统,由于文件系统支持快照,当写数据盘出错,数据不一致时,文件系统只需要回滚到上一次快照,并replay从上次快照开始的日志就可以,性能要比writeahead高,但是Linux下BTRFS和ZFS不稳定,线上生产环境几乎没人用。

    日志处理有三个阶段:

    1. 日志提交(journal submit):数据写入到日志盘,通常使用DirectIO+Libaio,一个单独的write_thread不断从队列取任务执行。
    2. 日志应用(journal apply):日志对应的修改更新到文件系统的文件上,此过程仅仅是写入到了Pagecache。
    3. 日志同步(journal commit):将文件系统的Pagecache脏页sync到磁盘上,此时数据已经持久化到数据盘,Journal便可以删除对应的数据,释放空间。

    7.4 幂等操作

    在机器异常宕机的情况下,Journal中的数据不一定全部都sync到了数据盘上,有可能一部分还在Pagecache,此时便需要在OSD重启时保证数据的一致性,对Journal做replay。FileStore将已经sync到数据盘的序列号记录在commit_op_seq中,replay的时候从commit_op_seq开始即可。

    但是在replay的时候,部分op可能已经sync到数据盘中,但是commit_op_seq却没有体现,序列化比其小,此时如果仍然replay,可能会出现非幂等操作,导致数据不一致。

    假设一个事务包含如下3个操作:

    1. clone a 到 b。
    2. 更新 a。
    3. 更新 c。

    假设上述操作都做完也已经持久化到数据盘上了,然后立马进程或者系统崩溃,此时sync线程还未来得及更新commit_op_seq,重启回放时,第二次执行clone操作就会clone到a新的数据版本,就会发生不一致。

    FileStore在对象的属性中记录最后操作的三元组(序列号、事务编号、OP编号),因为journal提交的时候有一个唯一的序列号,通过这个序列号, 就可以找到提交时候的事务,然后根据事务编号和OP编号最终定位出最后操作的OP。对于非幂等的操作,操作前先检查下,如果可以继续执行就执行操作,执行完之后设置一个guard。这样对于非幂等操作,如果上次执行过, 肯定是有记录的,再一次执行的时候check就会失败,就不继续执行。

    8 BlueStore

    Ceph早期的单机对象存储引擎是FileStore,为了维护数据的一致性,写入之前数据会先写Journal,然后再写到文件系统,会有一倍的写放大,而同时现在的文件系统一般都是日志型文件系统(ext系列、xfs),文件系统本身为了数据的一致性,也会写Journal,此时便相当于维护了两份Journal;另外FileStore是针对HDD的,并没有对SSD作优化,随着SSD的普及,针对SSD优化的单机对象存储也被提上了日程,BlueStore便由此应运而出。

    BlueStore最早在Jewel版本中引入,用于在SSD上替代传统的FileStore。作为新一代的高性能对象存储后端,BlueStore在设计中便充分考虑了对SSD以及NVME的适配。针对FileStore的缺陷,BlueStore选择绕过文件系统,直接接管裸设备,直接进行对象数据IO操作,同时元数据存放在RocksDB,大大缩短了整个对象存储的IO路径。BlueStore可以理解为一个支持ACID事物型的本地日志文件系统。

    8.1 架构设计

    BlueStore是一个事务型的本地日志文件系统。因为面向下一代全闪存阵列的设计,所以BlueStore在保证数据可靠性和一致性的前提下,需要尽可能的减小日志系统中双写带来的影响。全闪存阵列的存储介质的主要开销不再是磁盘寻址时间,而是数据传输时间。因此当一次写入的数据量超过一定规模后,写入Journal盘(SSD)的延时和直接写入数据盘(SSD)的延迟不再有明显优势,所以Journal的存在性便大大减弱了。但是要保证OverWrite(覆盖写)的数据一致性,又不得不借助于Journal,所以针对Journal设计的考量便变得尤为重要了。

    一个可行的方式是使用增量日志。针对大范围的覆盖写,只在其前后非磁盘块大小对齐的部分使用Journal,即RMW,其他部分直接重定向写COW即可。

    RWM(Read-Modify-Write):指当覆盖写发生时,如果本次改写的内容不足一个BlockSize,那么需要先将对应的块读上来,然后再内存中将原内容和待修改内容合并Merge,最后将新的块写到原来的位置。但是RMW也带来了两个问题: 一是需要额外的读开销; 二是如果磁盘中途掉电,会有数据损坏的风险。为此我们需要引入Journal,先将待更新数据写入Journal,然后再更新数据,最后再删除Journal对应的空间。

    COW(Copy-On-Write):指当覆盖写发生时,不是更新磁盘对应位置已有的内容,而是新分配一块空间,写入本次更新的内容,然后更新对应的地址指针,最后释放原有数据对应的磁盘空间。理论上COW可以解决RMW的两个问题,但是也带来了其他的问题: 一是COW机制破坏了数据在磁盘分布的物理连续性。经过多次COW后,读数据的顺序读将会便会随机读。 二是针对小于块大小的覆盖写采用COW会得不偿失。 是因为一是将新的内容写入新的块后,原有的块仍然保留部分有效内容,不能释放无效空间,而且再次读的时候需要将两个块读出来做Merge操作,才能返回最终需要的数据,将大大影响读性能。 二是存储系统一般元数据越多,功能越丰富,元数据越少,功能越简单。而且任何操作必然涉及元数据,所以元数据是系统中的热点数据。COW涉及空间重分配和地址重定向,将会引入更多的元数据,进而导致系统元数据无法全部缓存在内存里面,性能会大打折扣。

    基于以上设计理念,BlueStore的写策略综合运用了COW和RMW策略。 非覆盖写直接分配空间写入即可; 块大小对齐的覆盖写采用COW策略; 小于块大小的覆盖写采用RMW策略。整体架构设计如下图:

    • BlockDevice:物理块设备,使用Libaio、SPDK、io_uring操作裸设备,AsyncIO。
    • RocksDB:存储对象元数据、对象扩展属性Omap、磁盘分配器元数据。
    • BlueRocksEnv:抛弃了传统文件系统,封装RocksDB文件操作的接口。
    • BlueFS:小型的Append文件系统,实现了RocksDB::Env接口,给RocksDB用。
    • Allocator:磁盘分配器,负责高效的分配磁盘空间。
    • Cache:实现了元数据和数据的缓存。

    8.2 BlockDevice

    Ceph新的存储引擎BlueStore已成为默认的存储引擎,抛弃了对传统文件系统的依赖,直接管理裸设备,通过Libaio的方式进行读写。抽象出了 BlockDevice基类,提供统一的操作接口,后端对应不同的设备类型的实现(Kernel、NVME、PMEM)。

    • KernelDevice:通常使用Libaio或者io_uring,适用于HDD和SATA SSD。
    • NVMEDevice:通常使用SPDK用户态IO,提升IOPS缩短延迟,适用于NVME磁盘。
    • PMEMDevice:当做磁盘来用,使用libpmem库来操作。

    IO架构图如下所示:

    8.3 磁盘分配器

    BlueStore直接管理裸设备,那么必然面临着如何高效分配磁盘中的块。BlueStore支持基于Extent和基于BitMap的两种磁盘分配策略,有 BitMap分配器(基于Bitmap)Stupid分配器(基于Extent),原则上都是尽量顺序分配而达到顺序写。

    刚开始使用的是BitMap分配器,由于性能问题又切换到了Stupid分配器。之后Igor Fedotov大神重新设计和实现了 新版本BitMap分配器,性能也比Stupid要好,默认的磁盘分配器又改回了BitMap。

    新版本BitMap分配器以Tree-Like的方式组织数据结构,整体分为L0、L1、L2三层。每一层都包含了完整的磁盘空间映射,只不过是slot以及children的粒度不同,这样可以加快查找,如下图所示:

    新版本Bitmap分配器分配空间的大体策略如下:

    1. 循环从L2中找到可以分配空间的slot以及children位置。
    2. 在L2的slot以及children位置的基础上循环找到L1中可以分配空间的slot以及children位置。
    3. 在L1的slot以及children位置的基础上循环找到L0中可以分配空间的slot以及children位置。
    4. 在1-3步骤中保存分配空间的结果以及设置每层对应位置分配的标志位。

    新版本Bitmap分配器整体架构设计有以下几点优势:

    1. Allocator避免在内存中使用指针和树形结构,使用vector连续的内存空间。
    2. Allocator充分利用64位机器CPU缓存的特性,最大程序的提高性能。
    3. Allocator操作的单元是64 bit,而不是在单个bit上操作。
    4. Allocator使用3级树状结构,可以更快的查找空闲空间。
    5. Allocator在初始化时L0、L1、L2三级BitMap就占用了固定的内存大小。
    6. Allocator可以支持并发的分配空闲,锁定L2的children(bit)即可,暂未实现。

    BlueStore直接管理裸设备,需要自行管理空间的分配和释放。Stupid和Bitmap分配器的结果是保存在内存中的,分配结果的持久化是通过FreelistManager来做的。

    FreelistManager最开始有extent和bitmap两种实现,现在默认为bitmap实现,extent的实现已经废弃。空闲空间持久化到磁盘也是通过RocksDB的Batch写入的。FreelistManager将block按一定数量组成段,每个段对应一个k/v键值对,key为第一个block在磁盘物理地址空间的offset,value为段内每个block的状态,即由0/1组成的位图,1为空闲,0为使用,这样可以通过与1进行异或运算,将分配和回收空间两种操作统一起来。

    8.4 BlueFS

    RocksDB不支持对裸设备的直接操作,文件的读写必须实现rocksdb::EnvWrapper接口,RocksDB默认实现有POSIX文件系统的读写接口。而POSIX文件系统作为通用的文件系统,其很多功能对于RocksDB来说并不是必须的, 同时RocksDB文件结构层次比较简单,不需要复杂的目录树,对文件系统的使用也比较简单,只使用追加写以及顺序读随机读。为了进一步提升RocksDB的性能,需要对文件系统的功能进行裁剪,而更彻底的办法就是考虑RocksDB的场景量身定制一套本地文件系统,BlueFS也就应运而生。相对于POSIX文件系统有以下几个优点:

    1. 元数据结构简单,使用两个map(dir_map、file_map)即可管理文件的所有元数据。
    2. 由于RocksDB只需要追加写,所以每次分配物理空间时进行提前预分配,一方面减少空间分配的次数,另一方面做到较好的空间连续性。
    3. 由于RocksDB的文件数量较少,可以将文件的元数据全部加载到内存,从而提高读取性能。
    4. 多设备支持,BlueFS将存储空间划分了3个层次:Slow慢速空间(存放BlueStore数据)、DB高速空间(存放sstable)、WAL超高速空间(存放WAL、自身Journal),空间不足或空间不存在时可自动降级到下一层空间。
    5. 新型硬件支持,抽象出了block_device,可以支持Libaio、io_uring、SPDK、PMEM、NVME-ZNS。

    接口功能

    RocksDB是通过BlueRocksEnv来使用BlueFS的,BlueRocksEnv实现了文件读写和目录操作,其他的都继承自rocksdb::EnvWrapper。

    • 文件操作:追加写、顺序读(适用于WAL的读,也会进行预读)、随机读(sstable的读,不会进行预读)、重命名、sync、文件锁。
    • 目录操作:目录的创建、删除、遍历,目录只有一级,即 /a 、 /a/b、/a/b/c 为同一级目录,整体元数据map可表示为:map<string(目录名), map<string(文件名), file_info(文件元数据)>>。

    磁盘布局

    BlueFS的数据结构比较简单,主要包含三部分,superblock、journal、data。

    • superblock:主要存放BlueFS的全局信息以及日志的信息,其位置固定在BlueFS的头部4K。
    • journal:存放元数据操作的日志记录,一般会预分配一块连续区域,写满以后从剩余空间再进行分配,在程序启动加载的时候逐条回放journal记录,从而将元数据加载到内存。也会对journal进行压缩,防止空间浪费、重放时间长。压缩时会遍历元数据,将元数据重新写到新的日志文件中,最后替换日志文件。
    • data:实际的文件数据存放区域,每次写入时从剩余空间分配一块区域,存放的是一个个sstable文件的数据。

    元数据

    BlueFS元数据:主要包含:superblock、dir_map、file_map、文件到物理地址的映射关系。

    文件数据:每个文件的数据在物理空间上的地址由若干个extents表:一个extent包含bdev、offset和length三个元素,bdev为设备标识,因为BlueFS将存储空间设备划分为三层:慢速(Slow)空间、高速(DB)空间、超高速(WAL),bdev即标识此extent在哪块设备上,offset表示此extent的数据在设备上的物理偏移地址,length表示该块数据的长度。

    structbluefs_extent_t{uint64_toffset=0;uint32_tlength=0;uint8_tbdev;}// 一个sstable就是一个fnodestructbluefs_fnode_t{uint64_tino;uint64_tsize;utime_tmtime;uint8_tprefer_bdev;mempool::bluefs::vector<bluefs_extent_t>extents;uint64_tallocated;}

    按照9T盘、sstable 8MB,文件元数据80B来算,所需内存 9 * 1024 * 1024 / 8 * 80 / 1024 / 1024 = 90MB,说明把元数据全部缓存到内存并不会占用过多的内存。

    加载流程

    1. 加载superblock到内存。
    2. 初始化各存储空间的块分配器。
    3. 日志回放建立dir_map、file_map来重建整体元数据。
    4. 标记已分配空间:BlueFS没有像BlueStore那样使用FreelistManager来持久化分配结果,因为sstable大小固定从不修改,所以BlueFS磁盘分配需求都是比较同意和固定的。会遍历每个文件的分配信息,然后移除相应的磁盘分配器中的空闲空间,防止已分配空间的重复分配。

    读写数据

    读数据:先从dir_map和file_map找到文件的fnode(包含物理的extent),然后从对应设备的物理地址读取即可。

    写数据:BlueFS只提供append操作,所有文件都是追加写入。RocksDB调用完append以后,数据并未真正落盘,而是先缓存在内存当中,只有调用sync接口时才会真正落盘。

    1. open file for write
      打开文件句柄,如果文件不存在则创建新的文件,如果文件存在则会更新文件fnode中的mtime,在事务log_t中添加更新操作,此时事务记录还不会持久化到journal中。
    2. append file
      将数据追加到文件当中,此时数据缓存在内存当中,并未落盘,也未分配新的空间。
    3. flush data(写数据)
      判断文件已分配剩余空间(fnode中的 allocated - size)是否足够写入缓存数据,若不够则为文件分配新的空间;如果有新分配空间,将文件标记为dirty加到dirty_files当中,将数据进行磁盘块大小对其后落盘,此时数据已经写到硬盘当中,元数据还未更新,同时BlueFS中的文件都是追加写入,不存在原地覆盖写,就算失败也不会污染原来的数据。
    4. flush_and_sync_log(写元数据)
      从dirty_files中取到dirty的文件,在事务log_t中添加更新操作(即添加OP_FILE_UPDATE类型的记录),将log_t中的内容sync到journal中,然后移除dirty_files中已更新的文件。

    第3步是写数据、第4步是写元数据,都涉及到sync落盘,整体一个文件的写入需要两次sync,已经算是很不错了。

    8.5 对象IO

    BlueStore中的对象非常类似于文件系统中的文件,每个对象在BlueStore中拥有唯一的ID、大小、从0开始逻辑编址、支持扩展属性等,因此对象的组织形式,类似于文件也是基于Extent。

    BlueStore的每个对象对应一个Onode结构体,每个Onode包含一张extent-map,extent-map包含多个extent(lextent即逻辑的extent),每个extent负责管理对象内的一个逻辑段数据并且关联一个Blob,Blob包含多个pextent(物理的extent,对应磁盘上的一段连续地址空间的数据),最终将对象的数据映射到磁盘上。具体可参考BlueStore源码分析之对象IOBlueStore源码分析之事物状态机

    BlueStore中磁盘的最小分配单元是min_alloc_size,HDD默认64K,SSD默认16K,里面有2种磁盘分配的写类型(分配磁盘空间,数据还在内存):

    1. big-write:对齐到min_alloc_size的写我们称为大写(big-write),在处理是会根据实际大小生成lextent、blob,lextent包含的区域是min_alloc_size的整数倍,如果lextent是之前写过的,那么会将之前lextent对应的空间记录下来并回收。
    2. small-write:落在min_alloc_size区间内的写我们称为小写(small-write)。因为最小分配单元min_alloc_size,HDD默认64K,SSD默认16K,所以如果是一个4KB的IO那么只会占用到blob的一部分,剩余的空间还可以存放其他的数据。所以小写会先根据offset查找有没有可复用的blob,如果没有则生成新的blob。

    真正写磁盘时,有两种不同的写类型:

    1、simple-write:包含对齐覆盖写(COW)和非覆盖写,先把数据写入新的磁盘block,然后更新RocksDB里面的KV元数据,状态转换图如下:

    这图话的比较好,拿过来直接用了,如有侵权,联系删除。

    2、deferred-write:为非对齐覆盖写,先把数据作为WAL写RocksDB即先写日志,然后会进行RMW操作写数据到磁盘,最后CleanupRocksDB中的deferred-write的数据。

    这图话的比较好,拿过来直接用了,如有侵权,联系删除。

    3、simple-write + deferred-write:上层的一次IO很有可能同时涉及到simple-write和deferred-write,其状态机就是上面两个加起来,只不过少了deferred-write的写WAL一步,因为可以在simple-write写元数据时就一同把WAL写入RocksDB。

    9 未来规划

    随着硬件的不断发展,IO的速度越来越快,PMEM和NVME也逐渐成为了存储系统的主流选择,相比之下CPU的速度没有那么快了,反而甚至成为了系统的瓶颈。如何高效合理的利用新型硬件是分布式存储不得不面临的一个重大问题。Ceph传统的线程模型是多线程+队列的模型,一个IO从发起到完成要经历重重队列和不同的线程池,锁竞争、上下文切换和Cache Miss比较严重,也导致IO延迟迟迟降不下来。通过Perf发现CPU主要都耗在了锁竞争和系统调用上,Ceph自身的序列化和反序列化也比较消耗CPU,所以需要一套新的编程框架来解决上述问题。Seastar是一套基于future-promsie现代化高效的share-nothing的网络编程框架,从18年开始,Ceph社区便基于Seastar来重构整个OSD,项目代号 Crimson,来更好的解决上述问题。

    Crimson设计目标

    1. 最小化CPU开销。
    2. 减少跨核通信。
    3. 减少数据拷贝。
    4. Bypass Kernel,减少上下文切换。
    5. 支持新硬件:ZNS-NVME、PMEM等。

    线程模型

    性能对比

    测试RBD时,在达到同等iops和延迟时,crimson-osd的cpu比ceph-osd的cpu少了好几倍。

    BlueStore适配

    BlueStore目前是Ceph里性能比较高的单机存储引擎,从设计研发到稳定差不多持续了3年时间,足以说明研发一个单机存储引擎的时间成本是比较高的。由于BlueStore不符合Seastar的编程模型,所以需要对BlueStore适配,目前有两种方案:

    1. BlueStore-Alien:使用一个Alien Thread,使用Seastar的编程模型专门向Seastar-Reactor提交BlueStore的任务。
    2. BlueStore-Native:使用Seastar-Env来实现RocksDB的Rocksdb-Env,从而更原生的适配。

    但是由于RocksDB有自己的线程模型,外部不可控,所以无论怎么适配都不是最好的方案,理论上从0开始用基于Seastar的模型来写一个单机存储引擎是最完美的方案,于是便有了SeaStore,而BlueStore的适配也作为中间过渡方案,最多可用于HDD。

    SeaStore

    SeaStore是下一代的ObjectStore,适用于Crimson的后端存储,专门为了NVME设计,使用SPDK访问,同时由于Flash设备的特性,重写时必须先要进行擦除操作,也就是内部需要做GC,是不可控的,所以Ceph希望把Flash的GC提到SeaStore中来做:

    1. SeaStore的逻辑段(segment)理想情况下与硬件segment(Flash擦除单位)对齐。
    2. SeaStar是每个线程一个CPU核,所以将底层按照CPU核进行分段,每个核分配指定个数的segment。
    3. 当磁盘利用率达到阈值时,将少量的GC清理工作和正常的写流量一起做。
    4. 元数据使用B+数存储,而不是原来的RocksDB。
    5. 所有segment都是追加顺序写入的。

    10 参考资源

    1. https://github.com/ceph/ceph-v13.1.0
    2. https://zhuanlan.zhihu.com/分步试存储
    3. Ceph设计原理与实现-中兴通讯
    4. Ceph十年经验总结:文件系统是否适合做分布式文件系统的后端
    5. Sage: bluestore-a-new-storage-backend-for-ceph
    6. Crimson: a-new-ceph-osd-for-the-age-of-persistent-memory-and-fast-nvme-storage

    mysql 一棵 B+ 树能存多少条数据?

    $
    0
    0

    mysql 的InnoDB存储引擎 一棵B+树可以存放多少行数据?

    图片

    (答案在文章中!!)

    要搞清楚这个问题,首先要从InnoDB索引数据结构、数据组织方式说起。

    我们都知道计算机有五大组成部分:控制器,运算器,存储器,输入设备,输出设备。

    其中很重要的,也跟今天这个题目有关系的是存储器。

    我们知道万事万物都有自己的单元体系,若干个小单体组成一个个大的个体。就像拼乐高一样,可以自由组合。所以说,如果能熟悉最小单元,就意味着我们抓住了事物的本事,再复杂的问题也会迎刃而解。


    存储单元


    存储器范围比较大,但是数据具体怎么存储,有自己的最小存储单元。

    1、数据持久化存储磁盘里,磁盘的最小单元是扇区, 一个扇区的大小是 512个字节

    2、文件系统的最小单元是块, 一个块的大小是 4K

    3、InnoDB存储引擎,有自己的最小单元,称之为页, 一个页的大小是16K


    扇区、块、页这三者的存储关系?

    图片



    InnoDB引擎


    如果mysql部署在本地,通过命令行方式连接mysql,默认的端口  3306 ,然后输入密码即可进入

    mysql -u root -p

    查看InnoDB的页大小

    show variables like 'innodb_page_size';   

    图片

    mysql数据库中,table表中的记录都是存储在页中,那么一页可以存多少行数据?假如一行数据的大小约为1K字节,那么按  16K / 1K = 16,可以计算出一页大约能存放16条数据。

    mysql 的最小存储单元叫做“页”,这么多的页是如何构建一个庞大的数据组织,我们又如何知道数据存储在哪一个页中?

    如果逐条遍历,性能肯定很差。为了提升查找速度,我们引入了 B+树,先来看下 B+树的存储结构


    图片

    页除了可以存放 数据(叶子节点),还可以存放 健值和指针(非叶子节点),当然他们是有序的。这样的数据组织形式,我们称为索引组织表。

    如:上图中 page number=3的页,该页存放键值和指向数据页的指针,这样的页由N个键值+指针组成


    B+ 树是如何检索记录?

    • 首先找到根页,你怎么知道一张表的根页在哪呢?
    • 其实每张表的根页位置在表空间文件中是固定的,即page number=3的页
    • 找到根页后通过二分查找法,定位到id=5的数据应该在指针P5指向的页中
    • 然后再去page number=5的页中查找,同样通过二分查询法即可找到id=5的记录


    如何计算B+树的高度?


    InnoDB 的表空间文件中,约定 page number = 3表示主键索引的根页

    SELECT   
    b.name, a.name, index_id, type, a.space, a.PAGE_NO
    FROM
    information_schema.INNODB_SYS_INDEXES a,
    information_schema.INNODB_SYS_TABLES b
    WHERE
    a.table_id = b.table_id AND a.space <> 0
    and b.name like '%sp_job_log';

    图片

    从图中可以看出,每个表的主键索引的根页的page number都是3,而其他的二级索引page number为4

    在根页偏移量为 64的地方存放了该B+树的 page level。主键索引B+树的根页在整个表空间文件中的第3个页开始,所以算出它在文件中的偏移量: 16384*3 + 64 = 49152 + 64 =49216,前2个字节中。

    首先,找到MySql数据库物理文件存放位置:

    show global variables like "%datadir%" ;   

    图片


    hexdump工具,查看表空间文件指定偏移量上的数据:

    hexdump -s 49216 -n 10  sp_job_log.ibd   

    图片

    page_level 值是 1,那么 B+树高度为  page level + 1 = 2


    特别说明:

    • 查询数据库时,不论读一行,还是读多行,都是将这些行所在的整页数据加载,然后在内存中匹配过滤出最终结果。

    • 表的检索速度跟树的深度有直接关系,毕竟一次页加载就是一次IO,而磁盘IO又是比较费时间。 对于一张千万级条数B+树高度为3的表与几十万级B+树高度也为3的表,其实查询效率相差不大。



    一棵树可以存放多少行数据?


    假设B+树的深度为2

    这棵B+树的存储总记录数 =  根节点指针数 * 单个叶子节点记录条数


    那么指针数如何计算?

    假设主键ID为 bigint类型,长度为 8字节,而指针大小在InnoDB源码中设置为 6字节,这样一共 14字节

    那么一个页中能存放多少这样的组合,就代表有多少指针,即  16384 / 14 = 1170。那么可以算出一棵高度为2 的B+树,能存放  1170 * 16 = 18720 条这样的数据记录。

    同理:

    高度为3的B+树可以存放的行数 =   1170 * 1170 * 16 = 21902400

    千万级的数据存储只需要约3层B+树,查询数据时,每加载一页(page)代表一次IO。所以说,根据主键id索引查询约3次IO便可以找到目标结果。


    对于一些复杂的查询,可能需要走二级索引,那么通过二级索引查找记录最多需要花费多少次IO呢?

    图片

    首先,从二级索引B+树中,根据 name 找到对应的主键id

    图片

    然后,再根据主键id 从 聚簇索引查找到对应的记录。如上图所示,二级索引有3层,聚簇索引有3层,那么最多花费的IO次数是:3+3 = 6

    聚簇索引默认是主键,如果表中没有定义主键,InnoDB 会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。

    这也是为什么InnoDB表必须有主键,并且推荐使用整型的自增主键!!!

    InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上

    举例说明:

    1、若使用 "where id = 14"这样的条件查找记录,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。

    2、若对Name列进行条件搜索,则需要两个步骤:

    • 第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键值。
    • 第二步使用主键值在主索引B+树中再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)

    实战演示


    实际项目中,每个表的结构设计都不一样,占用的存储空间大小也各不相等。如何计算不同的B+树深度下,一个表可以存储的记录条数?

    我们以业务日志表  sp_job_log 为例,讲解详细的计算过程:

    1、查看表的状态信息

    show table status like 'sp_job_log'\G   

    图片

    图中看到 sp_job_log表的行平均大小为 153个字节

    2、查看表结构

    desc sp_job_log;   

    图片

    3、计算B+树的行数

    • 单个叶子节点(页)中的记录数 = 16K / 153 = 105
    • 非叶子节点能存放多少指针, 16384 / 14 = 1170
    • 如果树的高度为3,可以存放的记录行数 =  1170 * 1170 * 105 = 143,734,500


    最后加餐


    普通索引和唯一索引在查询效率上有什么不同?

    唯一索引就是在普通索引上增加了约束性,也就是关键字唯一,找到了关键字就停止检索。而普通索引,可能会存在用户记录中的关键字相同的情况,根据页结构的原理,当我们读取一条记录的时候,不是单独将这条记录从磁盘中读出去,而是将这个记录所在的页全部加载到内存中进行读取。InnoDB 存储引擎的页大小为 16KB,在一个页中可能存储着上千个记录,因此在普通索引的字段上进行查找也就是在内存中多几次 判断下一条记录的操作,对于 CPU 来说,这些操作所消耗的时间是可以忽略不计的。所以对一个索引字段进行检索,采用普通索引还是唯一索引在检索效率上基本上没有差别。



    灰度发布系统架构设计

    $
    0
    0

    灰度发布的定义

    互联网产品需要快速迭代开发上线,又要保证质量,保证刚上线的系统,一旦出现问题可以很快控制影响面,就需要设计一套灰度发布系统。

    灰度发布系统的作用,可以根据配置,将用户的流量导到新上线的系统上,来快速验证新的功能,而一旦出现问题,也可以马上的修复,简单的说,就是一套A/B Test系统。

    灰度发布允许带着bug上线,只要bug不是致命的,当然这个bug是不知道的情况下,如果知道就要很快的改掉。

    简单灰度发布系统的设计

    1.jpg

    灰度简单架构如上图所示,其中的必要组件如下:
    • 策略的配置平台,存放灰度的策略
    • 灰度功能的执行程序
    • 注册中心,注册的服务携带ip/Port/name/version


    有了上面三个组件,才算一个完整的灰度平台

    灰度的策略

    灰度必须要有灰度策略,灰度策略常见的方式有以下几种:
    • 基于Request Header进行流量切分
    • 基于Cookie进行流量切分
    • 基于请求参数进行流量切分


    举例:根据请求中携带的用户uid进行取模,灰度的范围是百分之一,那么uid取模的范围就是100,模是0访问新版服务,模是1~99的访问老版服务。

    灰度发布策略分为两类,单策略和组合策略:
    • 单策略:比如按照用户的uid、token、ip进行取模
    • 组合策略:多个服务同时灰度,比如我有A/B/C三个服务,需要同时对A和C进行灰度,但是B不需要灰度,这个时候就需要一个tag字段,具体实现在下文详述。


    灰度发布具体的执行控制

    在上面的简单灰度发布系统架构中我们了解到,灰度发布服务分为上游和下游服务,上游服务是具体的执行灰度策略的程序,这个服务可以是Nginx,也可以是微服务架构中的网关层/业务逻辑层,下面我们就来分析一下不同的上游服务,如何落地。

    Nginx

    如果上游服务是Nginx,那么就需要Nginx通过Lua扩展Nginx实现灰度策略的配置和转发,因为Nginx本身并不具备灰度策略的执行。

    通过Lua扩展实现了灰度策略的执行,但是问题又来了,Nginx本身并不具备接收配置管理平台的灰度策略,这个时候应该怎么办呢?

    解决方案:本地部署Agent(需要自己开发),接收服务配置管理平台下发的灰度策略,更新Nginx配置,优雅重启Nginx服务。

    网关层/业务逻辑层/数据访问层

    只需要集成配置管理平台客户端SDK,接收服务配置管理平台下发的灰度策略,在通过集成的SDK进行灰度策略的执行即可。

    灰度发布复杂场景

    下面举例两个稍微复杂的灰度发布场景,灰度策略假设都按照uid取模灰度百分之一的用户,看一下如何实现。

    场景1:调用链上同时灰度多个服务

    功能升级涉及到多个服务变动,网关层和数据访问层灰度,业务逻辑层不变,这个时候应该如何进行灰度?

    解决方案:

    经过新版本网关层的请求,全部打上tag T,在业务逻辑层根据tag T进行转发, 标记Tag T的请求全部转发到新版数据访问层服务上,没有tag T的请求全部转发到老版数据访问层上。
    2.jpg

    场景2:涉及数据的灰度服务

    涉及到数据的灰度服务,一定会使用到数据库,使用到数据库就会涉及到你使用数据库前后的表字段不一致,我老版本是A/B/C三个字段,新版本是A/B/C/D四个字段。这时新版的灰度,就不能往老版的数据库进行修改了,这个时候就需要把数据copy一份出来做这个事情了

    数据库其实并没有灰度的概念,这个时候我们只能把数据重新拷贝一份出来进行读和写,因为这时你的写必须是全量的(双写),不能说90%的数据写入到老版本,10%的数据写入到新版本,因为这个时候你会发现两个数据库的数据都不是全量的。

    离线全量复制数据的过程中一定会有数据丢失,这个时候就需要业务逻辑层写一份数据到MQ中,等数据同步完成之后,新版的数据访问层再将MQ的数据写入到新版本的DB中,实现数据的一致性,这个也是引入MQ的主要目的。
    3.jpg

    灰度过程中需要对两个数据库的数据进行对比,观察数据是否一致。这样不管是灰度失败,放弃新版DB,还是灰度成功切换到新版DB,数据都不会产生丢失。

    原文链接: https://www.toutiao.com/i6910008843955192323,作者:小杨互联网

      走出《女性贫困》

      $
      0
      0
      [cp]#哪本书带你走出了迷茫# 
      还是想要推荐一下这本书,尤其是推荐给年轻姑娘,即将读大学的,即将工作的,刚工作的都适合。

      这本书我说过很多次了,属于简单易懂很薄一本,最多2个小时,很快就看完了。

      它把日本女性贫困的原因总结了出来,比如过早的生育,比如没有学历,比如自己减负了“过多”责任挤压了自己的提升空间等等。其实很多原因放在国内也很适用。

      看书有各种各样的目的,推荐大家看这本,不是为了让你感叹社会多么不公平,别人多不幸,我觉得更多的是让大家了解生活中可能出现的坑和常见错误,这本书已经给你总结好了,你看到结果就可以进行分析了。

      了解分析过后,最大的作用一定是,找到一种对自己对生活的方向,规避风险。看完后如果仅仅只是悲伤、义愤填膺,没有警戒作用,我个人认为是没太大意义的。

      前几天有个tag“赚钱是不是最重要的”,赚钱可能不是最重要的,但赚钱很重要。人生的目的追求有很多,但是如果你温饱都做不到,想要求说别的非常困难。人可以不富有,但绝对不应该被贫困囚禁于活着仅限生存。

      这几年还有一个小感悟,就是一定要根据自己生活情况来分析情况,也少被社交媒体影响,很多时候只能坏了心情浪费时间。[/cp]

      [翻译]使用Spring Boot进行单元测试

      $
      0
      0

      原文地址: https://reflectoring.io/unit-...

      编写好的单元测试可以被看成一个很难掌握的艺术。但好消息是支持单元测试的机制很容易学习。

      本文给你提供在Spring Boot 应用程序中编写好的单元测试的机制,并且深入技术细节。

      我们将带你学习如何以可测试的方式创建Spring Bean实例,然后讨论如何使用 MockitoAssertJ,这两个包在Spring Boot中都为了测试默认引用了。

      本文只讨论单元测试。至于集成测试,测试web层和测试持久层将会在接下来的系列文章中进行讨论。

      代码示例

      本文附带的代码示例地址: spring-boot-testing

      使用 Spring Boot 进行测试系列文章

      这个教程是一个系列:

      1. 使用 Spring Boot 进行单元测试(本文)
      2. 使用 Spring Boot 和 @WebMvcTest 测试SpringMVC controller层
      3. 使用 Spring Boot 和 @DataJpaTest 测试JPA持久层查询
      4. 通过 @SpringBootTest 进行集成测试

      如果你喜欢看视频教程,可以看看 Philip的课程: 测试Spring Boot应用程序课程

      依赖项

      本文中,为了进行单元测试,我们会使用 JUnit Jupiter(Junit 5)MockitoAssertJ。此外,我们会引用 Lombok来减少一些模板代码:

      dependencies{
        compileOnly('org.projectlombok:lombok')
        testCompile('org.springframework.boot:spring-boot-starter-test')
        testCompile 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
        testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
      }

      MockitoAssertJ会在 spring-boot-test依赖中自动引用,但是我们需要自己引用 Lombok

      不要在单元测试中使用Spring

      如果你以前使用 Spring或者 Spring Boot写过单元测试,你可能会说我们不要在写单元测试的时候用 Spring。但是为什么呢?

      考虑下面的单元测试类,这个类测试了 RegisterUseCase类的单个方法:

      @ExtendWith(SpringExtension.class)
      @SpringBootTest
      class RegisterUseCaseTest {
      
        @Autowired
        private RegisterUseCase registerUseCase;
      
        @Test
        void savedUserHasRegistrationDate() {
          User user = new User("zaphod", "zaphod@mail.com");
          User savedUser = registerUseCase.registerUser(user);
          assertThat(savedUser.getRegistrationDate()).isNotNull();
        }
      
      }

      这个测试类在我的电脑上需要大概4.5秒来执行一个空的Spring项目。

      但是一个好的单元测试仅仅需要几毫秒。否则就会阻碍TDD(测试驱动开发)流程,这个流程倡导“测试/开发/测试”。

      但是就算我们不使用TDD,等待一个单元测试太久也会破坏我们的注意力。

      执行上述的测试方法事实上仅需要几毫秒。剩下的4.5秒是因为 @SpringBootTest告诉了 Spring Boot要启动整个Spring Boot 应用程序上下文。

      所以我们启动整个应用程序仅仅是因为要把 RegisterUseCase实例注入到我们的测试类中。启动整个应用程序可能耗时更久,假设应用程序更大、 Spring需要加载更多的实例到应用程序上下文中。

      所以,这就是为什么不要在单元测试中使用 Spring。坦白说,大部分编写单元测试的教程都没有使用 Spring Boot

      创建一个可测试的类实例

      然后,为了让 Spring实例有更好的测试性,有几件事是我们可以做的。

      属性注入是不好的

      让我们以一个反例开始。考虑下述类:

      @Service
      public class RegisterUseCase {
      
        @Autowired
        private UserRepository userRepository;
      
        public User registerUser(User user) {
          return userRepository.save(user);
        }
      
      }

      这个类如果没有 Spring没法进行单元测试,因为它没有提供方法传递 UserRepository实例。因此我们只能用文章之前讨论的方式-让Spring创建 UserRepository实例,并通过 @Autowired注解注入进去。

      这里的教训是:不要用属性注入。

      提供一个构造函数

      实际上,我们根本不需要使用 @Autowired注解:

      @Service
      public class RegisterUseCase {
      
        private final UserRepository userRepository;
      
        public RegisterUseCase(UserRepository userRepository) {
          this.userRepository = userRepository;
        }
      
        public User registerUser(User user) {
          return userRepository.save(user);
        }
      
      }

      这个版本通过提供一个允许传入 UserRepository实例参数的构造函数来允许构造函数注入。在这个单元测试中,我们现在可以创建这样一个实例(或者我们之后要讨论的Mock实例)并通过构造函数注入了。

      当创建生成应用上下文的时候,Spring会自动使用这个构造函数来初始化 RegisterUseCase对象。注意,在Spring 5 之前,我们需要在构造函数上增加 @Autowired注解,以便让Spring找到这个构造函数。

      还要注意的是,现在 UserRepository属性是 final修饰的。这很重要,因为这样的话,应用程序生命周期时间内这个属性内容不会再变化。此外,它还可以帮我们避免变成错误,因为如果我们忘记初始化该属性的话,编译器就报错。

      减少模板代码

      通过使用 Lombok@RequiredArgsConstructor注解,我们可以让构造函数自动生成:

      @Service
      @RequiredArgsConstructor
      public class RegisterUseCase {
      
        private final UserRepository userRepository;
      
        public User registerUser(User user) {
          user.setRegistrationDate(LocalDateTime.now());
          return userRepository.save(user);
        }
      
      }

      现在,我们有一个非常简洁的类,没有样板代码,可以在普通的 java 测试用例中很容易被实例化:

      class RegisterUseCaseTest {
      
        private UserRepository userRepository = ...;
      
        private RegisterUseCase registerUseCase;
      
        @BeforeEach
        void initUseCase() {
          registerUseCase = new RegisterUseCase(userRepository);
        }
      
        @Test
        void savedUserHasRegistrationDate() {
          User user = new User("zaphod", "zaphod@mail.com");
          User savedUser = registerUseCase.registerUser(user);
          assertThat(savedUser.getRegistrationDate()).isNotNull();
        }
      
      }

      还有部分确实,就是如何模拟测试类所依赖的 UserReposity实例,我们不想依赖真实的类,因为这个类需要一个数据库连接。

      使用Mockito来模拟依赖项

      现在事实上的标准模拟库是 Mockito。它提供至少两种方式来创建一个模拟 UserRepository实例,来填补前述代码的空白。

      使用普通 Mockito来模拟依赖

      第一种方式是使用Mockito编程:

      private UserRepository userRepository = Mockito.mock(UserRepository.class);

      这会从外界创建一个看起来像 UserRepository的对象。默认情况下,方法被调用时不会做任何事情,如果方法有返回值,会返回 null

      因为 userRepository.save(user)返回null,现在我们的测试代码 assertThat(savedUser.getRegistrationDate()).isNotNull()会报空指针异常(NullPointerException)。

      所以我们需要告诉 Mockito,当 userRepository.save(user)调用的时候返回一些东西。我们可以用静态的 when方法实现:

      @Test
      void savedUserHasRegistrationDate() {
        User user = new User("zaphod", "zaphod@mail.com");
        when(userRepository.save(any(User.class))).then(returnsFirstArg());
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();
      }

      这会让 userRepository.save()返回和传入对象相同的对象。

      Mockito为了模拟对象、匹配参数以及验证方法调用,提供了非常多的特性。想看更多, 文档

      通过 Mockito@Mock注解模拟对象

      创建一个模拟对象的第二种方式是使用 Mockito@Mock注解结合 JUnit Jupiter的 MockitoExtension一起使用:

      @ExtendWith(MockitoExtension.class)
      class RegisterUseCaseTest {
      
        @Mock
        private UserRepository userRepository;
      
        private RegisterUseCase registerUseCase;
      
        @BeforeEach
        void initUseCase() {
          registerUseCase = new RegisterUseCase(userRepository);
        }
      
        @Test
        void savedUserHasRegistrationDate() {
          // ...
        }
      
      }

      @Mock注解指明那些属性需要 Mockito注入模拟对象。由于 JUnit不会自动实现, MockitoExtension则告诉 Mockito来评估这些 @Mock注解。

      这个结果和调用 Mockito.mock()方法一样,凭个人品味选择即可。但是请注意,通过使用 MockitoExtension,我们的测试用例被绑定到测试框架。

      我们可以在 RegisterUseCase属性上使用 @InjectMocks注解来注入实例,而不是手动通过构造函数构造。 Mockito会使用特定的 算法来帮助我们创建相应实例对象:

      @ExtendWith(MockitoExtension.class)
      class RegisterUseCaseTest {
      
        @Mock
        private UserRepository userRepository;
      
        @InjectMocks
        private RegisterUseCase registerUseCase;
      
        @Test
        void savedUserHasRegistrationDate() {
          // ...
        }
      
      }

      使用AssertJ创建可读断言

      Spring Boot测试包自动附带的另一个库是 AssertJ。我们在上面的代码中已经用到它进行断言:

      assertThat(savedUser.getRegistrationDate()).isNotNull();

      然而,有没有可能让断言可读性更强呢?像这样,例子:

      assertThat(savedUser).hasRegistrationDate();

      有很多测试用例,只需要像这样进行很小的改动就能大大提高可理解性。所以,让我们在test/sources中创建我们自定义的断言吧:

      class UserAssert extends AbstractAssert<UserAssert, User> {
      
        UserAssert(User user) {
          super(user, UserAssert.class);
        }
      
        static UserAssert assertThat(User actual) {
          return new UserAssert(actual);
        }
      
        UserAssert hasRegistrationDate() {
          isNotNull();
          if (actual.getRegistrationDate() == null) {
            failWithMessage(
              "Expected user to have a registration date, but it was null"
            );
          }
          return this;
        }
      }

      现在,如果我们不是从 AssertJ库直接导入,而是从我们自定义断言类 UserAssert引入 assertThat方法的话,我们就可以使用新的、更可读的断言。

      创建一个这样自定义的断言类看起来很费时间,但是其实几分钟就完成了。我相信,将这些时间投入到创建可读性强的测试代码中是值得的,即使之后它的可读性只有一点点提高。我们编写测试代码就一次,但是之后,很多其他人(包括未来的我)在软件生命周期中,需要阅读、理解然后操作这些代码很多次。

      如果你还是觉得很费事,可以看看 断言生成器

      结论

      尽管在测试中启动Spring应用程序也有些理由,但是对于一般的单元测试,它不必要。有时甚至有害,因为更长的周转时间。换言之,我们应该使用更容易支持编写普通单元测试的方式构建Spring实例。

      Spring Boot Test Starter附带 MockitoAssertJ作为测试库。让我们利用这些测试库来创建富有表现力的单元测试!

      TCP之网络优化

      $
      0
      0

      上一篇文章我提到了Nagle算法,是为了解决报头大数据小从而导致网络利用率低的问题,这其实会带来新的问题。除此之外我们一起来看看tcp还会有什么优化策略呢!本文纯属学习记录,不完善或错误之处若指正将不胜感激。如有被误导的朋友,望海涵。


      首先我们先康康Nagle算法

      Nagle算法规则

      • (1)如果包长度达到MSS,则允许发送;
      • (2)如果该包含有FIN,则允许发送;
      • (3)设置了TCP_NODELAY选项,则允许发送;
      • (4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
      • (5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

      延迟ACK

      ACK机制中,在接收方收到一个包之后会先检查是否需要立即回应ACK,否则进入延迟ACK逻辑。 参考
      优点显而易见,提高了网络信道的利用率

      当Nagle遇见延迟ACK

      假想一个场景 MSS为8个中文字(最大报文长度)
      甲需要发送两个应用层报文给乙——“你好”!“我是甲”。显然这两个报文都是不足8的,但是由于“你好”满足Nagel的第4条规则所以会第一时间发送出去,由于ACK延迟,甲迟迟收不到乙的确认。于是等到超时发送。
      这会产生一个明显的延迟
      解决

      • 关闭Nagle算法,使用TCP套接字选项TCP_NODELAY可以关闭套接字选项(不推荐,有更多的优化策略)
      • 使用writev,而不是两次调用write,单个writev调用会使tcp输出一次而不是两次,只产生一个tcp分节,这是首选方法

      流量控制之滑动窗口

      滑动窗口是为了平衡发送方和接收方速率不匹配,发送窗口在连接建立时由双方商定。但在通信的过程中,一般由接收方反馈本身的缓冲区大小从而动态调节发送窗口(缓冲区)大小

      拥塞控制

      为了方便,我们假设主机A给主机B传输数据
      我们知道,两台主机在传输数据包的时候,如果发送方迟迟没有收到接收方反馈的ACK,那么发送方就会认为它发送的数据包丢失了,进而会重新传输这个丢失的数据包。 然而实际情况有可能此时有太多主机正在使用信道资源,导致网络拥塞了,而A发送的数据包被堵在了半路,迟迟没有到达B。这个时候A误认为是发生了丢包情况,会重新传输这个数据包。
      结果就是不仅浪费了信道资源,还会使网络更加拥塞。因此,我们需要进行拥塞控制

      慢开始和拥塞避免

      • 在建立连接之后是如何确定拥塞窗口的大小?(拥塞窗口和滑动窗口注意区别)
        • 第一种策略,第一次发送一个包,如果没有丢失就+1,以此类推
        • 第二种策略,第一次发送一个包,如果没有丢失就乘以2,以此类推
        实际上第一种方式增长过于缓慢,难以快速适应网络拥塞情况,而第二种方式指数型增长,很容易到达拥塞阈值。
        所以二者取其长,在前期使用指数增长,当到达某一个数值之后进行线性增长
        但是无论是指数增长还是线性增长最终都会到达一个MAX值,此时会重新以1开始启动并把阈值设置为MAX/2
        我们把确定拥塞窗口大小的过程中指数增长阶段称之为 慢开始,线性增长阶段称之为 拥塞避免

      快速重传与快速恢复

      前面说过了,当出现网络拥塞时会重启慢开始过程

      • 怎么判断网络拥塞?
        • 当网络拥塞时,会出现大量的丢包
      • 超时重发(丢包)一定是网络拥塞吗?
        • 网络拥塞会导致大量的包触发超时重发事件。而当一个单独的包出现损坏或者丢包时,也会导致超时重发,所以超时重发不一定是网络拥塞
      • 如何判断超时重发的原因
        • 网络拥塞会导致大量的丢包
        • 单个的丢包,由于延迟ACK规则,后序到达的每到达一个包,接收端都会回复相同的ACK。故当发送方接收到三个相同的ACK时,表明发生了单个的丢包

      单个丢包事件,当发送方接收到三个相同的ACK时,此时发送方不必等待序号为ACK-1包的超时,会立即重发。并把当前的阈值设置为MAX,新的阈值为MAX/2,以 拥塞窗口 = MAX/2 进行增长
      重发阶段我们称之为 快速重传,窗口参数的调整阶段称之为 快速恢复

      腾讯云联合毕马威发布《区域性银行数字化转型白皮书》

      $
      0
      0

        中证网讯(记者 齐金钊)6月10日,腾讯云和毕马威联合18家银行发布《区域性银行数字化转型白皮书》。该白皮书基于对18家区域性银行的深度访谈以及46家区域性银行的深度调研,揭示了区域性银行数字化转型的挑战、机遇和破局之道。

        白皮书提出,“深耕本地、错位竞争、巧借外力”是区域性银行数字化转型的三大突破口,“新连接、新智能、新基建、新敏捷”四大新数字化能力体系的构建则是区域性银行数字化转型落地的基础。同时,在实施路径上,白皮书建议区域性银行制定数字化能力及价值评估体系,做好投入和收益评估,同时可以采取试点模式,引入成熟方案,打造短期速赢标杆,实现逐步突破。

        腾讯云副总裁、腾讯云金融行业负责人郭仁声表示,不少区域性银行选择把数字化转型作为提升竞争力的关键手段,但不同银行数字化转型的背景、资源、能力并不完全相同,腾讯云联合毕马威发布白皮书,不试图下定义,而是通过同业实践,为银行家们提供策略参考。

        毕马威中国副主席、毕马威腾讯全球客户业务主管合伙人黄文楷表示,区域性银行的数字化不仅要体现在技术研发、系统建设等“硬实力”上,同时对组织、文化与人才等“软实力”的要求也很高。毕马威联合腾讯云,希望助力区域性银行在危机中育先机、于变局中开新局,通过数字化转型和科技创新在市场中建立竞争优势。

        据介绍,在腾讯云的支持下,已有多家区域性银行取得了数字化转型的阶段性成果。广州农商银行与腾讯云合作打造了全栈分布式金融云平台,全方位覆盖了基础设施建设、技术平台建设、业务平台建设、体系规范建设、架构规划、业务运营合作等领域。上海农商银行联合腾讯云构建业务、数据双中台,实现了业务能力共享和数据信息资产化。同时基于智慧中台整合各项AI组件和模型,打造了上海农商银行“智慧大脑”。江苏银行基于腾讯云孵化出的联邦学习应用服务打破数据孤岛,在保护隐私的同时,有效释放出了大数据生产力;龙江银行引入腾讯数据服务及伽利略解决方案,通过获客、业务线上化实现了零售业务的增长和突破。长沙银行以“弗兰社+呼啦+开放银行”平台为载体,实现了客户“吃喝玩乐美”全覆盖,5个月内新增用户20万,触达超过15万人次,带来存款、理财等金融业务超1亿元,实现了湖湘本土知名消费品牌客群的共享导流和生态共建。

        腾讯云介绍,目前公司已累计服务了一大批金融领域客户,包括150多家银行、数十家券商与保险机构,以及超过90%持牌消金和众多泛金融企业。其中,腾讯云分布式数据库TDSQL已服务近半国内TOP 20银行,TOP10银行中服务比例更是高达60%。腾讯云还与中国银行、建设银行、中国人保、中国银联、深证通、中银国际证券等头部金融机构合作构建了金融云平台。企业微信已服务17家国有大型银行和全国性股份制银行总行,覆盖比例超过90%。

      Viewing all 11848 articles
      Browse latest View live


      <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>