如何利用软件定义简化网络运营维护工作

一、为什么是软件定义?

在这个“软件定义一切”的时代(有人戏称为“SDx”),网络自然也是其涵盖的范畴之一,应运而生的SDN便是一个方向,至于如何理解SDN,笔者更愿从广义的角度来看,而不单单是其狭义定义中的三个特点,即:控转分离、集中控制、开放可编程。或者说狭义的SDN是为广义的SDN来服务的,对底层网络的变革,带来的更大的好处是在后期网络运营时,对流量的可视化、流量调度、监控报警等方面带来更多的可操作性、可实施性及便捷性。

二、网络运营会面临哪些问题?

当一张网络在线运行之后,技术人员更多的时间和精力会投入到网络的优化、变更、监控、报警等,总结起来几个方面:
– 可视化
– 操作简化
– 实时监控
– 快速定位
– 可感知

面临大量维护工作的同时,还要总结网络是否满足日益繁杂的业务要求,是否需要调整,是否需要优化,如何优化,如何规避等问题,这是迭代过程中的必然过程,正所谓“打江山容易,守江山难”。

三、传统网络技术环境有哪些弊端?

面临网络运营问题,传统网络技术环境多为网络设备“堆砌”而成,“盒子”由设备厂商所有,很难看到一个全量的、透明的网络世界,因此在实现这些网络运营需求的过程中,技术人员只能通过标准协议将有限的MIB信息读取至各种或开源或商业化的网络信息平台,从而实现有限程度的运营需求,究其原因,笔者认为主要集中在以下几点:
– 厂商私有化严重,相对闭源。
– 诸如snmp、netconf等标准化协议的限制,无法更好的满足技术需求。
– 各种网络工具软件灵活性不够、功能性不足、适配性欠缺,需要定制或二次开发,部署难度大。

网络从业人员在面临上述问题时,往往为了网络运营这一刚需,会搭建大量相对无关联的网络信息平台、自定义监控脚本,费时费力的同时也只能满足部分监控指标,且这些平台本身也需要技术人员不定期维护。

四、如何给网络“松绑”?

当前网络技术发展处于开源与闭源的过渡期,之所以这么说,是因为以SDN为代表新型网络技术还没有完全落地,与之对应的硬件产品也在市场化过程中,且需要市场的考虑及技术的沉淀,而以传统网络厂商为代表的闭源产品,也正在融入开放、开源的理念在其产品中,很多网络设备已支持OpenFlow等南向协议、开放gRPC接口,甚至你可以把一台交换机视作一台服务器,可在其本地编写/运行脚本,这些改善有助于在网络技术迭代的过程中,完善、加强网络运营指标,更好服务于业务需求。笔者这里提供几点部署建议,供参考:
– 在保留既有网络投入及网络架构的前提下,引入网络开源协议或技术。
– 将新老技术混合应用在网络环境中,这并不冲突。
– 尝试“过渡式”的技术实现方案,新老技术并行,如:传统网络技术承载业务,开源网络方案介入网络管理。

五、网络平稳过渡很重要

网络平稳的重要性不言而喻,网络作为所有业务生产者/消费者的“传输媒介”,其稳定性将直接影响产品体验,新技术的引入、网络的迭代一定要与旧技术“并行”,在不影响当前业务的前提下,改善网络运营环境。示例如下图:

  1. 网络设备通过OpenFlow、gRPC等将管理流量引入SDN控制端,通过面向北向接口的各种平台多维度分析(所见即所得)。
  2. 对具备研发能力的网络人员可基于开源的SDN CONTROLLER(ODL/ONOS)定制网络模型、流量分类、数据标记等。
  3. 与三方的开源网络平台对接,如API等方式,摆脱传统的只能SNMP上报流量的方式。

六、小结

网络新型技术的出现必将改变传统的网络实现方式,并对“网络世界”的视角变得更加全面,流量见得大而全,数据才能分析的透,这对网络更全面的优化、了解流量更全面的特点、深耕业务的发展方向提供更有意义的指导和建议。

TIDB在伴鱼的实践

一、背景介绍

伴鱼少儿英语是目前飞速成长的互联网在线英语教育品牌之一,旗下包括伴鱼绘本、伴鱼少儿英语、伴鱼自然拼读和伴鱼精读课等系列产品。伴鱼使用的数据库种类有Tidb,MongoDB,MySQL,Codis。在伴鱼,目前生产环境共有11套Tidb集群,服务于在线教学、绘本、消息、交易和少儿等众多核心业务。

目前线上Tidb部署版本统一为2.1.15。Tidb server和PD混部,机器配置:64C64G500G SAS;Tikv单独部署,机器配置:64C64G1.5T NVME

下面从伴鱼为何选择Tidb,伴鱼Tidb架构、伴鱼在使用Tidb过程中遇到的问题、我们是如何解决这些问题的以及Tidb在伴鱼的后续使用计划等几个方面进行阐述。

二、伴鱼为何选择Tidb

伴鱼发展之初,选择了MongoDB作为数据存储,但随着业务的快速发展,逐渐暴露出一些问题,比如业务对事务的需求、单表数据极速增长导致的性能问题以及集群实例容量问题等。因此,我们对新存储的诉求有以下几点:

集群高可用
MongoDB副本集架构支持高可用,不需要通过第三方程序来支持高可用,可以减少依赖和运维成本。因此对于新存储,我们也需要它天然支持高可用。

支持事务
目前我们生产使用的MongoDB 3.2版本对事务的支持较弱,而业务的很多场景需要事务的支持。

大容量、高吞吐
MongoDB副本集主节点永远只有一个,当业务快速发展时,可能出现写瓶颈,如果这时想扩展写就非常痛苦了。同时,机器的磁盘空间有限,在数据增长到一定量级时,也会出现集群容量瓶颈的问题。

不分表
不管MySQL还是MongoDB数据库,表数据达到一定的量级,读写性能都会下降。通过分表可以提高读写性能,但是业务代码需要维护比较复杂的分表逻辑和路由。

基于以上这些需求,我们通过调研,选择了当前流行的Tidb分布式数据库,主要原因包括:Tidb天然支持以上几点需求;同时Tidb已经在很多公司使用起来,经历了众多业务场景的历练,而且我们的业务使用场景也比较类似。

三、伴鱼Tidb架构

TiDB 集群主要包括三个核心组件:TiDB Server,PD Server 和 TiKV Server。

TiDB Server
TiDB Server负责接收SQL请求,处理SQL相关的逻辑,并通过PD找到存储计算所需数据的TiKV地址,与TiKV交互获取数据,最终返回结果。TiDB Server是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展。

PD Server
Placement Driver是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个Key存储在哪个TiKV节点);二是对TiKV集群进行调度和负载均衡(如数据的迁移、Raft group leader的迁移等);三是分配全局唯一且递增的事务ID。
PD通过Raft协议保证数据的安全性。Raft的leader server负责处理所有操作,其余的PD server仅用于保证高可用。

TiKV Server
TiKV Server负责存储数据,从外部看TiKV是一个分布式的提供事务的Key-Value存储引擎。存储数据的基本单位是Region,每个Region负责存储一个Key Range(从 StartKey到EndKey的左闭右开区间)的数据,每个TiKV节点会负责多个Region。TiKV使用Raft协议做复制,保持数据的一致性和容灾。副本以Region为单位进行管理,不同节点上的多个Region构成一个Raft Group,互为副本。数据在多个TiKV之间的负载均衡由PD调度,这里也是以Region为单位进行调度。

伴鱼Tidb架构
目前伴鱼Tidb的业务接入方式主要通过SLB接入,当TiDB-Server节点宕机,10s可以被负载均衡识别,自动剔除故障节点。PD和Tikv数据默认三副本,组件自身的高可用由raft算法协议保证。

四、伴鱼在使用Tidb过程中遇到的问题
伴鱼在使用Tidb之初,我们统一了集群使用的Tidb版本,规范了版本的配置,这样可以避免维护多个版本带来的潜在问题,同时有利于增强我们对特定版本的把控程度。我们生产环境的Tidb版本为2.1.15,随着业务的快速发展,主要遇到以下几个问题:

优化器选择索引不准确
在生产环境中,我们碰到过以下几种现象
1)单表数据30W+,查询请求并发约10+,某次业务上线,新增一个索引后,导致原有的查询索引选择错误,tikv实例所在机器cpu迅速被打满,引发故障。
2)线上某张大表,请求量比较大,偶尔出现个别条件走不到索引,导致全表扫描,从而引发接口响应时间的抖动,影响业务。
3)线上某张14亿的大表,查询条件区分度很高,某天出现特定条件突然走不到索引,导致全表扫描,引发故障。后面经过pingcap同学排查,系bug导致。

数据库性能问题定位慢
生产环境发生过一次今人印象深刻的故障,持续时间约一天。其实现在回过头来看,问题可以简单描述为:一个sql由于走错索引,数据扫描比较大,同时加上高峰期并发比较大,导致tikv cpu被打满,导致整个集群响应时间变高,引发故障。但是这次问题的定位,我们付出了很大的代价。

大数据同步问题
许多公司都有数据分析的需求,我们把上游各Tidb集群的数据通过Pump/Drainer汇聚到一个Tidb集群供大数据分析使用。在使用过程中,遇到数据不一致以及数据同步慢等问题。

五、我们如何解决在使用Tidb过程中遇到的问题

对于优化器选择索引不准确问题
在开发使用Tidb的过程中,对于数据量大的表和请求量比较大的表,我们强烈要求开发使用强制索引,防止在特殊情况下,请求走不到索引。同时,我们SQL审核平台,对新建表的索引数量都有严格的限制,防止研发同学建过多的索引。索引越少,理论上可以降低优化器选错索引的概率。

对于数据库性能问题的快速定位
Tidb兼容了MySQL协议,运维方式上跟MySQL有很多相似之处。我们认为,数据库的性能问题,绝大部分原因都是由慢SQL导致的,当然像数据库bug、业务异常流量等情况,在伴鱼还是比较少见的。所以,我们如何准实时收集分布式Tidb的慢日志、如何快速的做分析统计以及如何及时的告警,对于快速解决线上问题,甚至将性能风险扼杀在摇篮里至关重要。

1)如何快速收集Tidb慢日志
我们采用了业界比较成熟的开源日志采集/分析/存储架构,很好的解决了我们对慢日志的分析统计需求。

日志采集架构中,其中filebeat负责增量收集Tidb产生的慢日志,由于filebeat比较轻量,对线上性能基本无影响;kafka负责接收从filebeat采集过来的慢日志;logstash负责读取kafka中的慢日志并进行解析,转换成我们想要的kv键值对;最后解析后的数据入到es,供kibana查询分析统计。

对于Tidb的慢日志,我们重点关注慢日志中的某些特定字段,比如:
Time:表示日志打印时间。
DB:表示执行语句时使用的database。
Query_time:表示执行这个语句花费的时间。
Total_keys:表示Coprocessor扫过的key的数量。
Process_keys:表示Coprocessor处理的key的数量。相比total_keys,processed_keys不包含 MVCC的旧版本。
SQL:执行的sql语句。

2)如何快速的分析统计
logstash解析的数据入到es,通过kibana,我们可以很方便的对数据进行分析统计,比如:
1)近5分钟内,查询时间超过1s的请求/倒序排列/按表统计
2)近5分钟内,Process_keys大于5000的请求/倒序排列/按表统计
3)按照业务db统计慢日志数量
等等,同时我们还开发了慢日志分析报表平台,从各个维度(库/表/操作类型/慢日志数量/总时间/平均响应时间等)对慢日志进行统计分析,及时发现性能风险。

3)如何及时的告警
在伴鱼,一个db对应一个服务,所以告警都是在特定db下设置规则。目前,我们告警粒度是一分钟,主要基于以下三类规则告警:某个db的慢日志达到一定数量则告警;某个db下的请求时间超过500ms且达到一定数量则告警;某个db下的查询Process_keys大于1000且达到一定数量则告警。当然告警规则的设置不是一蹴而就的,需要根据不同的业务场景,不断的调整,最终达到一个比较合理的阀值。

对于大数据同步问题
我们通过pump/drainer将上游Tidb的数据同步到大数据Tidb集群,供大数据分析统计。在使用过程中,我们遇到以下几个问题,并逐一解决。
1)某天业务反馈,大数据集群某些表的数据少于线上集群。通过排查发现,上游某张表数据乱码,drainer同步下游报错,导致服务重启,重启时同步的位点不正确,导致上下游数据不正确。最后通过修复上游数据,重新同步解决。
2)drainer消费数据很慢,通过加大txn-batch/worker-count数量解决。
3)Tidb大数据集群raft store cpu长期居高临下,我们通过开启开启Region Merge和调整raft-base-tick-interval解决,最终raft store cpu利用率从90%下降到50%。

六、Tidb在伴鱼的后续展望
目前,我们在保证生产环境Tidb-2.1.15现有版本的稳定的同时,也在调研Tidb-3.0.x新版本的一些特性。从官方介绍,我们重点关注以下特性:
1)3.0与2.1版本相比,提升了大规模集群的稳定性。优化Raft副本之间的心跳机制,按照Region的活跃程度调整心跳频率,减小冷数据对集群的负担;热点调度策略支持更多参数配置,采用更高优先级,并提升热点调度的准确性;优化PD调度流程,提供调度限流机制;新增分布式GC 功能,提升GC的性能,降低大集群GC时间。
2)TiDB 3.0版本采用多种优化手段提升查询计划的稳定性。如新增Fast Analyze功能,提升收集统计信息的速度,降低集群资源的消耗及对业务的影响;新增SQL Plan Management功能,支持在查询计划不准确时手动绑定查询计划。
3)3.0与2.1版本相比,在查询性能上有很大的提升。如TiDB持续优化SQL执行器;TiKV新增多线程Raftstore和Apply功能,提升单节点内并发处理能力和资源利用率,降低延时,大幅提升集群写入能力。
因此,我们计划在2020年上半年,陆续将生产Tidb版本从2.1升级到3.0版本。

Kubernetes容器网络

Kubernetes容器网络

在kubernetes中要保证容器之间网络互通,网络至关重要.而kubernetes本身并没有具体实现网络,而是通过插件化的方式自由接入。在容器网络接入进来需要满足基本原则:

  • pod无论运行在任何节点都可以互相直接通信,而不需要借助NAT地址转换实现。
  • node与pod可以互相通信,在不限制的前提下,pod可以访问任意网络。
  • pod拥有独立的网络栈,pod看到自己的地址和外部看见的地址应该是一样的,并且同个pod内所有的容器共享同个网络栈。

容器网络基础

一个Linux容器的网络栈是被隔离在它自己的Network Namespace中,Network Namespace包括了:网卡(Network Interface),回环设备(Lookback Device),路由表(Routing Table)和iptables规则,对于服务进程来讲这些就构建了它发起请求和相应的基本环境。而要实现一个容器网络,离不开以下linux网络功能:

  • 网络命名空间: 将独立的网络协议栈隔离到不同的命令空间中,彼此间无法通信。
  • Veth Pair: Veth设备对的引入是为了实现在不同网络命名空间的通信,总是以两张虚拟网卡(veth peer) 的形式成对出现的。并且,从其中一端发出的数据,总是能在另外一端收到。
  • Iptables/Netfilter: Netfilter负责在内核中执行各种定义的规则(过滤、修改、丢弃等),运行在内核中;Iptables模式是在用户模式下运行的进程,负责协助维护内核中Netfilter的各种规则表;通过二者的配合来实现整个Linux网络协议栈中灵活的数据包处理机制
  • 网桥: 网桥是一个二层网络虚拟设备,类似交换机,主要功能是通过学习而来的Mac地址将数据帧转发到网桥的不同端口上.

  • 路由: Linux系统包含一个完整的路由功能,当IP层在处理数据发送或转发的时候,会使用路由表来决定发往哪里。

基于以上的基础,同宿主机的容器时间如何通信呢?
我们可以简单把他们理解成两台主机,主机之间通过网线连接起来,如果要多台主机通信,我们通过交换机就可以实现彼此互通。在容器中,以上的实现是通过docker0网桥,凡是连接到docker0网桥的容器,就可以通过它来进行通信。要想容器能够连接到docker0网桥,我们只需要类似网线的虚拟设备Veth Pair来把容器连接到网桥上即可。

我们启动一个容器:

$ docker run -d --name c1  hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh

然后查看网卡设备:

$ docker exec -it c1  /bin/sh
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:14 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1172 (1.1 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

可以看到其中有一张eth0的网卡,它就是veth peer其中的一端的虚拟网卡。然后通过route -n 查看容器中的路由表,eth0也正是默认路由出口。所有对172.17.0.0/16网段的请求都会从eth0出去。
我们再来看Veth peer的另一端,我们查看宿主机的网络设备:

# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:6aff:fe46:93d2  prefixlen 64  scopeid 0x20<link>
        ether 02:42:6a:46:93:d2  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.100.0.2  netmask 255.255.255.0  broadcast 10.100.0.255
        inet6 fe80::5400:2ff:fea3:4b44  prefixlen 64  scopeid 0x20<link>
        ether 56:00:02:a3:4b:44  txqueuelen 1000  (Ethernet)
        RX packets 7788093  bytes 9899954680 (9.2 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5512037  bytes 9512685850 (8.8 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 32  bytes 2592 (2.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 32  bytes 2592 (2.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth20b3dac: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::30e2:9cff:fe45:329  prefixlen 64  scopeid 0x20<link>
        ether 32:e2:9c:45:03:29  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

我们可以看到,容器对应的Veth peer 另一端是宿主机上的一块虚拟网卡叫veth20b3dac,并且可以通过brctl 查看网桥信息看到这张网卡是在docker0上。

# brctl show
docker0     8000.02426a4693d2   no      veth20b3dac

然后我们再启动一个容器,从第一个容器是否能ping通第二个容器.

$ docker run -d --name c2 hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh
$ docker exec -it c1 /bin/sh
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.291 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.129 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.142 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.169 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.194 ms
^C
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.129/0.185/0.291 ms

可以看到,能够ping通,其原理就是我们ping 目标IP172.17.0.3时,会匹配到我们的路由表第二条规则,网关为0.0.0.0,这就意味着是一条直连路由,通过二层转发到目的地。要通过二层网络到达172.17.0.3,我们需要知道它的Mac地址,此时就需要第一个容器发送一个ARP广播,来通过IP地址查找它的mac地址。此时Veth peer另外一段是docker0网桥,它会广播到所有连接它的veth peer 虚拟网卡去,然后正确的虚拟网卡收到后会响应这个ARP报文,然后网桥再回给第一个容器.
以上就是同宿主机不同容器通过docker0通信,如下图所示:

默认情况下,通过network namespace限制的容器进程,本质上是通过Veth peer设备和宿主机网桥的方式,实现了不同network namespace 的数据交换。

与之类似地,当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。

跨主机网络通信

在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。为了解决这个问题,社区中出现了很多网络方案。同时k8s为了更好的控制网络的接入,推出了CNI即容器网络的API接口。它是k8s中标准的一个调用网络实现的接口,kubelet通过这个API来调用不同的网络插件以实现不同的网络配置,实现了这个接口的就是CNI插件,它实现了一系列的CNI API接口。目前已经有的包括flannel、calico、weave、contiv等等。

实际上CNI的容器网络通信流程跟前面的基础网络一样,只是CNI维护了一个单独的网桥来代替 docker0。这个网桥的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。cni的设计思想就是:Kubernetes 在启动 Infra 容器之后,就可以直接调用 CNI 网络插件,为这个 Infra 容器的 Network Namespace配置符合预期的网络栈。

CNI插件三种网络实现模式:

  • overlay 模式是基于隧道技术实现的,整个容器网络和主机网络独立,容器之间跨主机通信时将整个容器网络封装到底层网络中,然后到达目标机器后再解封装传递到目标容器。不依赖与底层网络的实现。实现的插件有flannel(UDP、vxlan)、calico(IPIP)等等

  • 三层路由模式中容器和主机也属于不通的网段,他们容器互通主要是基于路由表打通,无需在主机之间建立隧道封包。但是限制条件必须依赖大二层同个局域网内。实现的插件有flannel(host-gw)、calico(BGP)等等

  • underlay网络是底层网络,负责互联互通。 容器网络和主机网络依然分属不同的网段,但是彼此处于同一层网络,处于相同的地位。整个网络三层互通,没有大二层的限制,但是需要强依赖底层网络的实现支持.实现的插件有calico(BGP)等等

我们看下路由模式的一种实现flannel Host-gw:

如图可以看到当node1上container-1要发数据给node2上的container2时,会匹配到如下的路由表规则:

10.244.1.0/24 via 10.168.0.3 dev eth0

表示前往目标网段10.244.1.0/24的IP包,需要经过本机eth0出去发往的下一跳ip地址为10.168.0.3(node2).然后到达10.168.0.3以后再通过路由表转发到cni网桥,进而进入到container2。

以上可以看到host-gw工作原理,其实就是在每个node节点配置到每个pod网段的下一跳为pod网段所在的node节点IP,pod网段和node节点ip的映射关系,flannel保存在etcd或者k8s中。flannel只需要watch 这些数据的变化来动态更新路由表即可.

这种网络模式最大的好处就是避免了额外的封包和解包带来的网络性能损耗。缺点我们也能看见主要就是容器ip包通过下一跳出去时,必须要二层通信封装成数据帧发送到下一跳。如果不在同个二层局域网,那么就要交给三层网关,而此时网关是不知道目标容器网络的(也可以静态在每个网关配置pod网段路由)。所以flannel host-gw必须要求集群宿主机是二层互通的。

而为了解决二层互通的限制性,calico提供的网络方案就可以更好的实现,calico 大三层网络模式与flannel 提供的类似,也会在每台宿主机添加如下格式的路由规则:

<目标容器IP网段> via <网关的IP地址> dev eth0

其中网关的IP地址在不同场景有不同的意思,如果宿主机是二层可达那么就是目的容器所在的宿主机的IP地址,如果是三层不同局域网那么就是本机宿主机的网关IP(交换机或者路由器地址).

不同于flannel通过k8s或者etcd存储的数据来维护本机路由信息的做法,calico是通过BGP动态路由协议来分发整个集群路由信息。

BGP全称是 Border Gateway Protocol边界网关协议,linxu原生支持的、专门用于在大规模数据中心为不同的自治系统之间传递路由信息。只要记住BGP简单理解其实就是实现大规模网络中节点路由信息同步共享的一种协议.而BGP这种协议就能代替flannel 维护主机路由表功能。

calico 主要由以下几个部分组成:

  • calico cni插件: 主要负责与kubernetes对接,供kubelet调用使用。
  • felix: 负责维护宿主机上的路由规则、FIB转发信息库等.
  • BIRD: 负责分发路由规则,类似路由器.
  • confd: 配置管理组件。

除此之外,calico还和flannel host-gw不同之处在于,它不会创建网桥设备,而是通过路由表来维护每个pod的通信,如下图所示:

可以看到calico 的cni插件会为每个容器设置一个veth pair设备,然后把另一端接入到宿主机网络空间,由于没有网桥,cni插件还需要在宿主机上为每个容器的veth pair设备配置一条路由规则,用于接收传入的IP包,路由规则如下:

10.92.77.163 dev cali93a8a799fe1 scope link

以上表示发送10.92.77.163的IP包应该发给cali93a8a799fe1设备,然后到达另外一段容器中。

有了这样的veth pair设备以后,容器发出的IP包就会通过veth pair设备到达宿主机,然后宿主机根据路由规则的下一条地址,发送给正确的网关(10.100.1.3),然后到达目标宿主机,在到达目标容器.

10.92.77.0/24 via 10.100.1.3 dev bond0 proto bird

这些路由规则都是felix维护配置的,而路由信息则是calico bird组件基于BGP分发而来。calico实际上是将集群里所有的节点都当做边界路由器来处理,他们一起组成了一个全互联的网络,彼此之间通过BGP交换路由,这些节点我们叫做BGP Peer。

需要注意的是calico 维护网络的默认模式是 node-to-node mesh ,这种模式下,每台宿主机的BGP client都会跟集群所有的节点BGP client进行通信交换路由。这样一来,随着节点规模数量N的增加,连接会以N的2次方增长,会集群网络本身带来巨大压力。
所以一般这种模式推荐的集群规模在50节点左右,超过50节点推荐使用另外一种RR(Router Reflector)模式,这种模式下,calico 可以指定几个节点作为RR,他们负责跟所有节点BGP client建立通信来学习集群所有的路由,其他节点只需要跟RR节点交换路由即可。这样大大降低了连接数量,同时为了集群网络稳定性,建议RR>=2.

以上的工作原理依然是在二层通信,当我们有两台宿主机,一台是10.100.0.2/24,节点上容器网络是10.92.204.0/24;另外一台是10.100.1.2/24,节点上容器网络是10.92.203.0/24,此时两台机器因为不在同个二层所以需要三层路由通信,这时calico就会在节点上生成如下路由表:

10.92.203.0/24 via 10.100.1.2 dev eth0 proto bird

这时候问题就来,因为10.100.1.2跟我们10.100.0.2不在同个子网,是不能二层通信的。这之后就需要使用Calico IPIP模式,当宿主机不在同个二层网络时就是用overlay网络封装以后再发出去。如下图所示:

IPIP模式下在非二层通信时,calico 会在node节点添加如下路由规则:

10.92.203.0/24 via 10.100.1.2 dev tunnel0

可以看到尽管下一条任然是node的IP地址,但是出口设备却是tunnel0,其是一个IP隧道设备,主要有Linux内核的IPIP驱动实现。会将容器的ip包直接封装宿主机网络的IP包中,这样到达node2以后再经过IPIP驱动拆包拿到原始容器IP包,然后通过路由规则发送给veth pair设备到达目标容器。

以上尽管可以解决非二层网络通信,但是仍然会因为封包和解包导致性能下降。如果calico 能够让宿主机之间的router设备也学习到容器路由规则,这样就可以直接三层通信了。比如在路由器添加如下的路由表:

10.92.203.0/24 via 10.100.1.2 dev interface1

而node1添加如下的路由表:

10.92.203.0/24 via 10.100.1.1 dev tunnel0

那么node1上的容器发出的IP包,基于本地路由表发送给10.100.1.1网关路由器,然后路由器收到IP包查看目的IP,通过本地路由表找到下一跳地址发送到node2,最终到达目的容器。这种方案,我们是可以基于underlay 网络来实现,只要底层支持BGP网络,可以和我们RR节点建立EBGP关系来交换集群内的路由信息。

以上就是kubernetes 常用的几种网络方案了,在公有云场景下一般用云厂商提供的cni plugin即可,大多数情况都是大二层子网内可以使用flannel host-gw这种更简单的方案,对于三层可以使用flannel vxlan 或者calico ipip,而私有物理机房环境中,Calico bgp项目更加适合。根据自己的实际场景,再选择合适的网络方案。

参考

  • https://github.com/coreos/flannel/blob/master/Documentation/backends.md
  • https://coreos.com/flannel/
  • https://docs.projectcalico.org/getting-started/kubernetes/
  • https://www.kancloud.cn/willseecloud/kubernetes-handbook/1321338

监控后传

这些年随着互联网的进步,监控系统也越来越完善,那接下来就谈谈如何理解监控,以及监控要做到什么

最近一直在思考,监控到底该是一个什么样子,为什么要监控?

为什么要监控,我认为监控一个系统有多个原因

  1. 分析长期趋势
    通过对监控样本数据的持续收集和统计,对监控指标进行长期趋势分析
  2. 周期的比较(环比)
    通过跨时间的比较,或者是观察实验组与控制组之间的区别
  3. 报警
    出现故障,需要立刻修复,或者可能很快会出故障,需要提前介入查看
  4. 监控大盘(黄金指标)
    系统的展示服务的一些基本问题,具体参考4个黄金指标
  5. 回溯分析
    经过对长期数据存储分析,并对数据进行多维度的回溯分析,能够帮助我们透视化了解系统的异常情况,主动发现潜在隐患

监控系统的核心本质是什么

大家都知道,无论故障发生的概率有多小,只要有出现的可能,它总会复现。

海恩法则指出:每一起严重事故的背后,必然有29次轻微事故和300期未遂先兆以及1000起事故隐患。

法则强调俩点:
一是事故的发生是量的积累的结果
二是再好的技术,再完美的规章,在实际操作层面,也无法取代人自身的素质和责任心

因此我们需要对自己质疑

  1. 什么东西出故障了(现象)
  2. 为什么出故障(原因)

<<SRE:Goole运维解密>> 一书指出,监控系统需要能够有效的支持白盒监控和黑盒监控

可能有点懵,简单介绍一下 白盒监控、黑盒监控。

白盒监控大量依赖对系统内部信息的监测,如系统日志、抓取提供指标信息的http节点等,因此可以监测到即将发生的问题及哪些重试所掩盖的问题等。

黑盒监控是面向现象的,代表了目前正在发生的,而非预测会发生的问题,即(系统现在有故障)。

监控系统的4个黄金指标(延迟、流量、错误、饱和度)

延迟:服务处理某个请求所需要的时间
流量:使用系统中的某个高层次的指标对系统负载需求所进行的度量
错误:请求失败的速率
饱和度:服务容量有多“满”

如果我们的度量所有这4个黄金指标,同时在某个指标出现故障时发出警报,能做到这些,服务的监控就基本差不多了。

Data rooms: let your business be successful | data room

Want more features? Work with partners better and organize projects more proficiently? Do you care about data protection and cell work with all of them? Then you ought to pay attention to . They may be used by normally from the Fortune-500 list, since the mentioned tool allows businesses to act on maximum!

More features and mobility

Virtual data rooms are a cloud storage area that allows a modern business to operate without any obstacles. Firstly, the development allows you to exchange data and work with them in a safe function. You will be able to share commercial and confidential documents with associates and clients around the world. Secondly, the platform permits faster and easier to determine transactions of any complexness, as well as execute examinations and audits. Thirdly, it will be easy and convenient for you to entice a specialist and organize the work of a remote control team.

It might be worth saying that data rooms allow you to quickly and efficiently work with papers. You will spend even less time on routine responsibilities and emphasis more in decision making and management top quality. associated with business even more mobile, deal with more automated and much easier, and all techniques are more secure. Indeed, it really is safety which is most important element of stable creation and powerful functioning.

More Trustworthiness and Security for Data

Data rooms have been built to wonderful web app security standards. This means that solutions and functions are the most efficient. The coders used the most modern strategies of encryption and key storage space, the most relevant anti-virus systems. As a result, the reliability of data rooms corresponds to the consistency of banking systems. Not merely the software is extremely secure, nonetheless also data centers. They may have strict physical access handles, as well as wonderful safety protocols in case of flame, flood or perhaps earthquake. And, of course , current file backups.

Do not forget that one of the most important aspects of control is that you simply. Before sending each file, it is you who will collection the get parameters, added restrictions. Plus the user who all receives the file will need to confirm his identity in many stages and will also be able to job only inside the parameters set for him. It is important to know that you can often cancel use of a file, regardless if it has already been downloaded. Each and every one user activities with files are saved in a wonderful journal, that you will have get.

Extra offers and best service plan

Electronic data rooms are a great way for you to do more, with less attempt. You can make the operation of the company much easier and more rewarding. High-quality technical support will also provide you with this. Ask questions and seek specialist advice whenever you want. Also, if the need takes place, they will be capable to help you with the development of unique functions, digitization and organization of documents. You can always count on the prompt image resolution of concerns and support for your organization.

If you want to know even more about best virtual data room, then simply just start using them. This can be carried out completely free with respect to thirty in the event you activate quality mode. Not only will you learn the info, but likewise check the top quality, simplicity and effectiveness on the software.

Gitlab ci 调试笔记 (一)

Gitlab ci 调试笔记 (一)

通常情况下,我们对ci的认识都停留在构建工具这一层面,对于许多使用方面的细节并不关注,所以我想写一下近段时间修改ci文件的一些细节。

1.release分支的问题

build_prod:
  stage: build_prod
  only:
    - /^release-.+/

通常我们会这样写线上的构建条件,但如果某些情况下,有一个分支被命名为release-xxx,就会错误的触发线上环境构建。

我们可以这样修改:

build_prod:
  stage: build_prod
  only:
    variables:
      - $CI_COMMIT_TAG =~ /^release-.+/

如果构建分支有tag,则ci会自动设置”CI_COMMIT_TAG”这个环境变量,在only中使用环境变量去判断,从而确保只有release-xxx的tag会触发构建。

2.手动操作

ci作为一款自动化工具,通常情况下所有的操作都由某些事件的触发而自动执行和完成,但有时,我们希望能受控的执行一些操作

deploy_prod:
  when: manual
  only:
    variables:
      - $CI_BUILD_TAG =~ /^release-.+/

只需要添加”when: manual”即可达到我们的期望,满足条件时,在ci/cd面板上会显示执行按钮,点击该按钮来执行我们期望的操作。

3.使用ssh密钥

ci能执行shell,那么,能不能ssh连接其他服务器,执行一些操作呢,显然是可以的,只需要配置一下。

使用ssh有一些前提,比如ssh密钥,同时还需要信任服务器(这一步我们在本地通常都是随手输入y信任)

我们将ssh密钥预先生成好,并放置到环境变量里待用,同时将KNOWN_HOSTS也放置到环境变量里。

publish:
  before_script:
    - 'which ssh-agent || ( apt-get update -y &amp;&amp; apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - &gt; /dev/null
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" &gt; ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts

似乎有点长,我们来解读一下。

首先判断一下ssh-agent是否存在,不存在则安装一下,
安装完成后运行ssh-agent,并将密钥和HOSTS添加进来,就完成了配置工作,接下来做什么就自行发挥了,帮你commit & push代码也是可以的。


本期内容不多,等下期再完善一下

如何设计一个好的前端架构

前言

什么时候好的前端架构,就是当有新的开发人员接触项目是,不会在理解数据跟踪及其背后体现的UI路径时不至遇上太多的问题。

什么样式好的架构

  • 易于管理
  • 方便理解
  • 规划清晰
  • 更新便捷
  • 组件化程度高
  • 流程方便

页面

页面代表着web应用中不同的目标,页面目录中的文件或者目录,代表着路由路径的目的地。这样,当我们通过路由并拆分出组件时,就能够便捷的径路与页面文件关联起来。
* 仅包含路由入口文件以及其所需要组建的关联
* 入口文件不应该包含完整逻辑,应该见逻辑根据功能拆分至不同的组件
* 规范命名,因为该文件代表着打包后的文件与路由组件

组件

组件越小,就越易于处理。将UI拆分成一个个小的组件。代码越少,我们对代码的掌控能力就越强,调试与必要时的更新就会越简单。可以通过以下方式:
* 将公共组件统一放到一个目录中
* 将每个文件的组件进行分类,确保其中不包含公共代码组件
* 尝试对组件进行概括,以便以后能在不同的场景中使用
* 将彼此相关的组件划分在一起。确保这些组件不会在目录之外的组件中使用

辅助函数

辅助函数应该强大且中立,辅助函数应该与渲染逻辑区分开,仅在需要使用的时候引用。其作用在于:
* 处理特定组件的逻辑
* 与浏览器规范相关
* 处理从后端接收到的数据,使其适用于业务
* 将公共的辅助函数划分到一起,便于管理

API服务

API服务是指负责在参数特定的情况下,调用服务器以获取数据的代码。我们不应该直接从UI逻辑中调用服务。因为如果我们需要在很多位置实现相同的API调用,name对不同位置进行修改迭代将变得非常困难。
* 应该将API服务进行封装,单独做一个服务来实现
* 应将从服务器接收的数据直接返回给组件
* 应该能接收配置或者变量等,作为API服务的必要参数进行传递

Config

Config 当中应包含web应用运行所在的环境具体配置。确保将配置与实际代码拆分出来。
* 使用不同的文件对应不同的环境类型
* 根据获取不同的资源类型而有所不同

路由

路由是保障web应用使用体验的主要方式,路由决定这我们在应用中需要显示的不同页面的URL格式或者模式。
* 路由的命名应该尽可能简短
* 尽可能保持路由的正确顺序

Static文件

Static文件是指未包含在逻辑当中的文件。
* 应该根据其类型进行分组
* 尽可能降低文件体积

其他

  • 如果是在用npm管理的话,package.json 应该有完善的相关命令,来保证开发人员流程畅通
  • readme 要写的足够详尽,因为一个开发如果要想了解一个项目的话,都会先阅读readme

以上就是我总结的一些想法,现在前端发展迅速,整个设局对于项目架构的思路也是日新月异,我只是希望我写的这些能起到一些帮助。

Android Lint扫描规则说明(二)

主要内容

对Android Studio支持的六类Android Lint规则, 本文主要对Performance包含的32个项的说明,主要内容都是文档翻译,适当加一些自己的感想。

分类详细说明

高效使用资源

UnusedIds

未被使用的资源id,在layout文件中定义了资源ID从未被使用过,但有时候它们可以让layout更容易阅读,没有必要删除未使用的资源id。

Overdraw

过度绘制:一个绘制区域被绘制的次数多于一次。

如果给一个root view设置了背景图,就要给它设置一个background=nulltheme,否则绘制过程会先绘制themebackground,然后再绘制设置的背景图,完全覆盖之前绘制的theme.background,这是 过度绘制

这个检测器依赖于根据扫描Java代码找出哪些布局文件与哪些Activity相关联,目前它使用的是一种不精确的模式匹配算法。因此,可能会因错误地推断布局与活动的关联而给出错误的提醒。

如果想把一个背景图应用在多个页面上,可以考虑自定义theme,并把背景图设置在theme里,在layout中设置theme代替设置background。如果背景图中有透明的部分,并且希望他和theme的背景有层叠效果,那么可以选择先把两个背景合并成一个背景图之后,在定义到theme里。

VectorPath

关于SVG的使用,给出一篇参考文章:Android vector标签 PathData 画图超详解,Android Studio可以创建使用SVG绘制出的drawable图像资源。

UselessLeaf

没有包含任何View,也没有设置背景的Layout是多余的,可以去掉。让界面更趋于扁平,嵌套更高效。

UselessParent

如果一个包含ViewLayout没有兄弟层级的Layout,而他的外部ViewGroup又不是ScrollView或者root级别,那么这个Layout可以移除,让他包含的View直接包含在它的父层级的Layout中。让界面更趋于扁平,嵌套更高效。

TooDeepLayout

Layout嵌套过深会影响性能,考虑使用平铺类型的Layout代替。默认最深的View嵌套是10层,也可以通过环境变量ANDROID_LINT_MAX_DEPTH进行设置。System.getenv("ANDROID_LINT_MAX_DEPTH");语句获取,如何设置还没找到。

TooManyViews

Layout内有太多的View:一个Layout文件内有过多的View会影响性能。考虑使用复合drawables或其他技巧来减少这个布局中的视图数量。默认最多的数量是80个,可以通过环境变量ANDROID_LINT_MAX_VIEW_COUNT进行设置。据说这个变量可以用System.getenv("ANDROID_LINT_MAX_DEPTH");语句获取,如何设置还没找到。

NestedWeights

Weight嵌套:使用非0layout-weight值,需要Layout被测量两次,如果一个包含非0值的LinearLayout被嵌套在另一个包含非0值的LinearLayout内部,那么,测量次数就会呈指数级增长。

DuplicateDivider

这个主要是讲RecyclerView的分割线,com.android.support:recyclerview-v7 提供了一个类DividerItemDecoration设置分割线样式,这个类在早期的版本内没有包含,所以在更新为新的版本后,可以使用这个类重新设置分割线。
具体使用,参考文章:Android RecyclerView 使用完全解析

MergeRootFrame

FrameLayout在一个layout文件中是root且没有使用background或者padding等属性,通常使用一个merge标签代替FrameLayout会更高效。但是这要看上下文设置,所以在替换之前要确认你已经理解了merge标签的工作原理

UnusedResources

未使用的资源:多指的是drawable类型的资源。多余的drawable资源会让APP变大,编译过程变长。

InefficientWeight

当LinearLayout只有一个Widget且使用了android:layout_weight时,定义对应的width/height的值为0dp,Widget就会自动占满剩余空间。因为不需要预先计算自己的尺寸,这种方式更高效。

高效的设置

DisableBaselineAlignment

在使用LinearLayout实现空间的按比例分割时,LinearLayout的空间用layout_weight属性在所包含的几个layout中间分割,那么应该设置被分割LinearLayoutbaseLineAligned="false",这样可以加快分割空间所做的运算。

LogConditional

LogConditional:使用android.util.Log打印调试日志,一般只会在DEBUG模式下使用,在release是不需要打印调试日志的,在buildToolsVersion大于等于17时, BuildConfig提供两个一个DEBUG常量来标记是否处于DEBUG模式,我们可以用if(BuildConfig.DEBUG){}包裹调试日志语句,这样编译器会在编译生成release包时,删除这些语句。如果真的需要在release模式下打印调试日志,可以使用@SuppressLint("LogConditional")注解告诉编译器在release包中保留这些日志信息。

UnpackedNativeCode

APP使用System.loadLibrary()加载Native库时,android 6.0或者更新的版本可以在Manifest文件中application标签中添加属性android:extractNativeLibs="false",这样可以提交加载速度,降低APP占用的存储空间。

更高效的替代方案

FloatMath

不要使用FloatMath类进行数学计算,推荐使用Math类。

Android早期版本因为浮点运算性能的原因,推荐使用FloatMath代替Math类进行数学计算。随着硬件和系统的发展,这个问题已经不复存在,甚至经过JIT优化之后的Math类运算速度会比FloatMath更快,所以,在Android F以上版本的系统上,可以直接使用Math类,而不是FloatMath。

UseValueOf

某些类构造新对象时,建议使用工厂方法,而不是new关键字声明新的对象。例如,new Intger(0)就可以使用Integer.valueOf(0)替代,工厂方法会使用更少的内存,因为它会让值相等的对象使用同一个实例。

ViewHolder

在给ListViewGradView之类的列表实现Adapter时,不能每次getView调用都去inflate一个新的layout,如果接口参数中给出了一个可以复用的View对象,就可以使用这个对象而不是重新生成。这个应该都很熟悉,也很简单基础了。

UseSparseArrays

KeyInteger类型的HashMap可以使用SparseArray代替,性能更好。可以使用替代HashMap的有SparseBooleanArray、SparseIntArray、SparseLongArray和泛型类SparseArray,每个对应的类型代表Value的类型。如果在某些情况一定要用HashMap实现,则可以用@SuppressLint注解抑制Lint检查。

WakelockTimeout

关于week lock的使用,这里提供一篇博客文章:Android 功耗分析之wakelock

UseCompoundDrawables

在一个TextView的四周有只具有展示作用的ImageView时,建议删除ImageView改用compound drawables:drawableTop, drawableLeft, drawableRight,drawableBottom,drawablePadding替代方案实现。

有关泄漏的提醒

Recycle

缺少recycle()调用:许多资源例如:TypedArrays, VelocityTrackers在使用完之后需要调用recycle()方法回收资源。

ViewTag

4.0版本系统之前,View.setTag(int, Object)的实现方式中,会把Object存储在一个静态的map里并且使用的是强引用。这就意味着如果这个Object包含了对Context对象的引用,这个Context就是泄漏了。

传递一个View做参数,这个View就能提供一个对创建它的Context的引用。类似的,View holders内包含View,也会有Context与这个View相关联。

HandlerLeak

Handler引用泄漏:声明Handler的子类如MyHandler为内部类,如果MyHandler类对象关联Looper.getMainLooper()或者Looper.getMainLooper().getQueue()时,会阻止无用的外部类对象被垃圾回收,导致泄漏。如果对应main thread 的关联,就不会有这个问题。

应对方法,声明MyHandler为静态内部类,并用WeakReference的方式持有一个外部类对象,MyHandler使用这个对象操作外部类的属性和方法。

DrawAllocation

绘制过程中的内存分配:避免在布局绘制过程中分配内存给新的对象。因为这些操作调用频率比较高,频繁分配内存会唤起垃圾回收,中断UI绘制,导致卡顿。

StaticFieldLeak

非静态内部类具有对其外部类对象的隐式引用。

如果外部类Fragment或者Activity,那么这个引用意味着长时间运行的处理程序/加载器/任务(handler/loader/task)将持外部类对象的引用,从而防止外部类对象被回收。

同理,长时间运行的处理程序/加载器/任务(handler/loader/task)对Fragment或者Activity的直接引用,也会造成泄漏。

ViewModel类应该禁止引用View或者non-application类型的Context对象。

代码提醒

AnimatorKeep

属性动画默认支持的属性如下面列表。如果超出这些范围,会通过反射调用本地定义的函数。声明一个属性动画对象例如:ObjectAnimator.ofFloat(view, "rotation", 0, 360) 中的“rotation”就是要操作的属性,如果属性不在下面的列表中例如ObjectAnimator.ofFloat(view, "position", 0, 360),就需要本地定义一个对应的方法setPosition(float position),并且这个方法需要加上@keep注解,防止被当做无用方法清理掉。

    static {
        PROXY_PROPERTIES.put("alpha", PreHoneycombCompat.ALPHA);
        PROXY_PROPERTIES.put("pivotX", PreHoneycombCompat.PIVOT_X);
        PROXY_PROPERTIES.put("pivotY", PreHoneycombCompat.PIVOT_Y);
        PROXY_PROPERTIES.put("translationX", PreHoneycombCompat.TRANSLATION_X);
        PROXY_PROPERTIES.put("translationY", PreHoneycombCompat.TRANSLATION_Y);
        PROXY_PROPERTIES.put("rotation", PreHoneycombCompat.ROTATION);
        PROXY_PROPERTIES.put("rotationX", PreHoneycombCompat.ROTATION_X);
        PROXY_PROPERTIES.put("rotationY", PreHoneycombCompat.ROTATION_Y);
        PROXY_PROPERTIES.put("scaleX", PreHoneycombCompat.SCALE_X);
        PROXY_PROPERTIES.put("scaleY", PreHoneycombCompat.SCALE_Y);
        PROXY_PROPERTIES.put("scrollX", PreHoneycombCompat.SCROLL_X);
        PROXY_PROPERTIES.put("scrollY", PreHoneycombCompat.SCROLL_Y);
        PROXY_PROPERTIES.put("x", PreHoneycombCompat.X);
        PROXY_PROPERTIES.put("y", PreHoneycombCompat.Y);
    }
ObsoleteSdkInt

无用的SDK版本检查:Android SDK的版本更新比较快,许多API的使用都需要通过检查SDK版本防止出现not found之类的崩溃。在APP迭代的过程中提升了minSdkVersion的值就会导致部分SDK版本检查不再需要。

这种SDK版本检查会引起不必要的资源搜索。

DevModeObsolete

以前,文档中建议在productFlavors中创建一个dev product。设定minSdkVersion 21,在开发过程中激活multidexing加速构建过程。现在已经不需要这么做了,在新版的IDE和Gradle插件中,会自动地识别所连接设备的API level,如果链接的设备API level大于等于21,就会自动打开multindexing,就跟之前设置了dev product的效果一样。

参考:Enable Android MultiDex

ObsoleteLayoutParam

无用的LayoutParam:当给Widget使用了所在Layout没有提供的LayouParam时,会有这个提示。这种情况一般出现在修改Layout类型时没有同时修改内部Widget的LayoutParam设置或者把一个Widget从一个Layout拷贝到另一个不同类型的Layout内部。

这种无用的LayoutParam在运行时会引起无效的属性解析,也会误导阅读这些代码的人。所以应该把这些无用的属性删除掉。

其他

WearableBindListener
UseOfBundledGooglePlayServices

js实现一键打印网页中所有的图片

最近写了一个小工具,可以一键打印网页中所有的图片链接,效果如下:

实现思路主要分为两部分,一是如何获取网页中所有的图片链接,二是如何在浏览器的控制台打印图片。封装形式用了便捷的书签的方式,可以点一个标签来运行。

(代码链接在最后)

获取网页中所有图片链接

网页中图片主要有三种形式:一是img标签,二是css中的背景图片,三是在style中设置的背景图片。我们分别获取一下这三种图片:

img标签

用dom查询的api获取所有的img标签,返回src的数组

function getDomImage() {
    let imgList = [].slice.call(document.querySelectorAll('img')).map(item => item.src);
    return imgList;
}

有style属性的元素的背景图片

首先通过*[style]选出所有有style属性的标签,然后把内容拼成一个css格式的字符串。之后对这个字符串使用正则匹配url()中的链接,然后放到数组中返回。

function getStyleImage() {
    const imgList = [];
    let styleEles = [].slice.call(document.querySelectorAll("*[style]"));
    styleEles && styleEles.map(styleEle => {
        const styleStr = Object.entries(styleEle.style).filter(item => item[1]).map(item => item[0] + ':' + item[1]).join(';');
        let styleImages = styleStr.match(/url\((.*)\)/g);
        styleImages = styleImages && styleImages.map(item => item.replace(/url\(['"]*([^'"]*)['"]*\)/,'$1'));
        if(styleImages) imgList.push(...styleImages);
    });
    return imgList;
}

css中的背景图片

首先选出所有的style元素,然后获取textContent,之后也是通过正则匹配url()中的链接,然后放入数组返回。

function getCssImage() {
    const styleEles = document.querySelectorAll('style');
    return [].slice.call(styleEles).map(styleEle => {
        const css = styleEle.textContent;
        const cssImages = css.match(/url\((.*)\)/g);
        return cssImages && cssImages.map(item => item.replace(/url\((.*)\)/,'$1')) || [];
    });
}

去重

获取到这三种图片之后,合并到一个数组中。

    function getImages() {
        return getDomImage().concat(...getCssImage()).concat(...getStyleImage());
    }

但现在的数组中可能有重复的图片,因为一个图片可能在页面上出现多次。我们可以通过set来去重。

function uniqueArr(arr) {
    return Array.from(new Set(arr))
}

控制台打印图片

现在有了所有图片的链接,下一步就是打印到控制台了。浏览器console支持%c指定css样式,可以通过background-image的方式来设置图片。这是一种hack的方式。

先打印了一堆的空格,留出空间来显示背景图,然后在这段空白的文字区域实现图片。

    function formatConsole(list) {

        if (window.console) {
            var cons = console;
            if (cons) {
                list.forEach(item => {
                    cons.log("%c\n                                                                                    ", "font-size:100px;background:url('"+ item+"') no-repeat;background-size:contain;");
                    cons.log(item);
                });
            }
        }
    }

这三步完成之后组合调用一下:

let imgs = getImages();
imgs = uniqueArr(imgs);
formatConsole(imgs);

现在获取网页所有图片并打印到控制台的功能已经完成了。

浏览器新建标签

功能已经完成了,可是怎么使用呢。开发一个chrome插件可以,而且不止可以打印到控制台,还可以传到服务器之类的,能做更多的事情。

但我们现在只想打印到控制台,可以使用一种简单的方式:浏览器书签。

在浏览器中新建一个书签,比如内容为javascript:alert(1);,那么你在书签栏点击这个标签的时候就会执行这段js。

基于此,我们只需要把上面的代码再包装一层就可以用了:

javascript: (function() {
    //xxx
})()

总结

至此,我们已经实现了一键打印网页中所有的图片。可以新建一个书签,然后内容复制下面链接中的代码,之后点击书签栏的书签就可以运行了。

代码链接

console-all-image

vue组件原型调用

相信很多人用vuejs构建应用时都会用到一些全局方法,比如发ajax请求时喜欢用axios挂载到vue原型上,如下:

// 引入vue和axios
import Vue from 'vue'
import axios from 'axios'
// 然后挂载到原型上
Vue.prototype.$axios = axios

// 用axios.get()方法可以这样用
this.$axios.get()

这样确实方便,不用每个用到axios的组件都去引入
类似如此,当我们要用到一些操作dom的方法时要怎么做呢,上面的例子纯属js的封装,没有涉及到dom;下面我用一个全局提示组件为例,类似element-ui的message组件为大家演示一遍如何封装一个包含操作dom的的全局组件的,步骤主要有3步:

1, 在componenets/Message 目录下新建一个Message.vue组件
<template>
<transition name="fade">
    <div class="message" :class="type" v-show="show">
      <i class="icon"></i>
      <span class="text">{{text}}</span>
    </div>
</transition>
</template>

<script type="text/ecmascript-6">
  export default {
    name: 'message',
    props: {
      type: {
        type: String,
        default: 'info',
      },
      text: {
        type: String,
        default: ''
      },
      show: {
        type: Boolean,
        default: false
      }
    }
  }
</script>

<style scoped lang="stylus">
//......
</style>
2, 在componenets/Message目录准备一个index.js
import Message from './Message.vue'

const MESSAGE = {
  duration: 3000, // 显示的时间 ms
  animateTime: 300, // 动画时间,表示这个组件切换show的动画时间
  install(Vue) {
    if (typeof window !== 'undefined' && window.Vue) {
      Vue = window.Vue
    }
    Vue.component('Message', Message)

    function msg(type, text, callBack) {
      let msg
      let duration = MESSAGE.duration
      if (typeof text === 'string') {
        msg = text
      } else if (text instanceof Object) {
        msg = text.text || ''
        if (text.duration) {
          duration = text.duration
        }
      }
      let VueMessage = Vue.extend({
        render(h) {
          let props = {
            type,
            text: msg,
            show: this.show
          }
          return h('message', {props})
        },
        data() {
          return {
            show: false
          }
        }
      })
      let newMessage = new VueMessage()
      let vm = newMessage.$mount()
      let el = vm.$el
      document.body.appendChild(el) // 把生成的提示的dom插入body中
      vm.show = true
      let t1 = setTimeout(() => {
        clearTimeout(t1)
        vm.show = false  //隐藏提示组件,此时会有300ms的动画效果,等动画效果过了再从body中移除dom
        let t2 = setTimeout(() => {
          clearTimeout(t2)
          document.body.removeChild(el) //从body中移除dom
          newMessage.$destroy()
          vm = null // 设置为null,好让js垃圾回收算法回收,释放内存

          callBack && (typeof callBack === 'function') && callBack() 
      // 如果有回调函数就执行,没有就不执行,用&&操作符,
      // 只有&&左边 的代码为true才执行&&右边的代码,避免用面条代码:
        }, MESSAGE.animateTime)
      }, duration)
    }

// 挂载到vue原型上,暴露四个方法
    Vue.prototype.$message = {
      info(text, callBack) {
        if (!text) return
        msg('info', text, callBack)
      },
      success(text, callBack) {
        if (!text) return
        msg('success', text, callBack)
      },
      error(text, callBack) {
        if (!text) return
        msg('error', text, callBack)
      },
      warning(text, callBack) {
        if (!text) return
        msg('warning', text, callBack)
      }
    }
  }
}
export default MESSAGE

上面的代码关键点就是用Vue.extend()构造出一个Vue子类实例,(注意我这里模板渲染只用到render函数,没有用template选项,因为template选项 要求装Vue时要加入模板编译器那块代码,用render函数更加简洁,只需要装运行时版本,Vue体积更加小);然后调用$mount()方法生成需要的dom,再拿到对应的$el,实例内部自己维护插入dom和移除dom的操作,对外暴露了四个方法info、success、error、warning方便不同的场景调用;类似的组件还有confrim组件、alert组件等,大同小异。

3,在main.js中引入components/Message/index.js,以插件形式安装

最后,当你需要用的时候就直接,特别适合在ajax回调函数里面用来提示

import Vue from 'vue'
import vMessage from './components/Message/index' 
Vue.use(vMessage)

this.$message.info('普通消息') 
this.$message.error('错误消息') 
this.$message.warning('警告消息') 
this.$message.success('成功消息')