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

10个常见的Redis面试

$
0
0

导读:在程序员面试过程中Redis相关的知识是常被问到的话题。作为一名在互联网技术行业打击过成百上千名的资深技术面试官,本文作者总结了面试过程中经常问到的问题。十分值得一读。

作者简介:钱文品(老钱),互联网分布式高并发技术十年老兵,目前任掌阅科技资深后端工程师。熟练使用 Java、Python、Golang 等多种计算机语言,开发过游戏,制作过网站,写过消息推送系统和MySQL 中间件,实现过开源的 ORM 框架、Web 框架、RPC 框架等

Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行各种刁难。作为一名在互联网技术行业打击过成百上千名【请允许我夸张一下】的资深技术面试官,看过了无数落寞的身影失望的离开,略感愧疚,故献上此文,希望各位读者以后面试势如破竹,永无失败!

Redis有哪些数据结构?

字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。

如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。

如果你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。

使用过Redis分布式锁么,它是什么回事?

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

这时候对方会告诉你说你回答得不错,然后接着问如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!对方这时会显露笑容,心里开始默念:摁,这小子还不错。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用过Redis做异步队列么,你是怎么用的?

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

如果对方追问redis如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。

如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

Redis如何做持久化的?

bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,优先使用aof来恢复内存的状态,如果没有aof日志,就会使用rdb文件来恢复。


如果再问aof文件过大恢复时间过长怎么办?你告诉面试官,Redis会定期做aof重写,压缩aof文件日志大小。如果面试官不够满意,再拿出杀手锏答案,Redis4.0之后有了混合持久化的功能,将bgsave的全量和aof的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。这个功能甚至很多面试官都不知道,他们肯定会对你刮目相看。


如果对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

Pipeline有什么好处,为什么要用pipeline?

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制了解么?

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

是否使用过Redis集群,集群的原理是什么?

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。


相关阅读:



高可用架构

改变互联网的构建方式

长按二维码 关注「高可用架构」公众号



Kafka系列之-Kafka监控工具KafkaOffsetMonitor配置及使用 - CSDN博客

$
0
0

  KafkaOffsetMonitor是一个可以用于监控Kafka的Topic及Consumer消费状况的工具,其配置和使用特别的方便。源项目Github地址为: https://github.com/quantifind/KafkaOffsetMonitor。 
  最简单的使用方式是从Github上下载一个最新的 KafkaOffsetMonitor-assembly-0.2.1.jar,上传到某服务器上,然后执行一句命令就可以运行起来。但是在使用过程中有可能会发现页面反应缓慢或者无法显示相应内容的情况。据说这是由于jar包中的某些js等文件需要连接到网络,或者需要翻墙导致的。网上找的一个修改版的KafkaOffsetMonitor对应jar包,可以完全在本地运行,经过测试效果不错。下载地址是: http://pan.baidu.com/s/1ntzIUPN,在此感谢一下贡献该修改版的原作者。链接失效的话,可以博客下方留言联系我。

一、KafkaOffsetMonitor的使用

  因为完全没有安装配置的过程,所以直接从KafkaOffsetMonitor的使用开始。 
  将KafkaOffsetMonitor-assembly-0.2.0.jar上传到服务器后,可以新建一个脚本用于启动该应用。脚本内容如下:

java -cp KafkaOffsetMonitor-assembly-0.2.0.jar \
    com.quantifind.kafka.offsetapp.OffsetGetterWeb \
    --zk m000:2181,m001:2181,m002:2181 \
    --port 8088 \
    --refresh 10.seconds \
    --retain 2.days
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

  各参数的作用可以参考一下Github上的描述:

  • offsetStorage valid options are ”zookeeper”, ”kafka” or ”storm”. Anything else falls back to ”zookeeper”
  • zk the ZooKeeper hosts
  • port on what port will the app be available
  • refresh how often should the app refresh and store a point in the DB
  • retain how long should points be kept in the DB
  • dbName where to store the history (default ‘offsetapp’)
  • kafkaOffsetForceFromStart only applies to ”kafka” format. Force KafkaOffsetMonitor to scan the commit messages from start (see notes below)
  • stormZKOffsetBase only applies to ”storm” format. Change the offset storage base in zookeeper, default to ”/stormconsumers” (see notes below)
  • pluginsArgs additional arguments used by extensions (see below)

      启动后,访问 m000:8088端口,可以看到如下页面: 
       这里写图片描述
      在这个页面上,可以看到当前Kafka集群中现有的Consumer Groups。

    在上图中有一个Visualizations选项卡,点击其中的Cluster Overview可以查看当前Kafka集群的Broker情况 
    这里写图片描述

      接下来将继续上一篇Kafka相关的文章 Kafka系列之-自定义Producer,在最后对Producer进行包装的基础上,分别实现一个简单的往随机Partition写messge,以及自定义Partitioner的Producer,对KafkaOffsetMonitor其他页面进行展示。

二、简单的Producer

1、新建一个Topic

  首先为本次试验新建一个Topic,命令如下:

bin/kafka-topics.sh \
    --create \
    --zookeeper m000:2181 \
    --replication-factor 3 \
    --partition 3 \
    --topic kafkamonitor-simpleproducer
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2、新建SimpleProducer代码

  在上一篇文章中提到的Producer封装Github代码的基础上,写了一个往kafkamonitor-simpleproducer发送message的java代码。

import com.ckm.kafka.producer.impl.KafkaProducerToolImpl;
import com.ckm.kafka.producer.inter.KafkaProducerTool;

/**
 * Created by ckm on 2016/8/30.
 */
public class SimpleProducer {
    public static void main(String[] args) {
        KafkaProducerTool kafkaProducerTool = new KafkaProducerToolImpl();
        int i = 0;
        String message = "";
        while (true) {
            message = "test-simple-producer : " + i ++;
            kafkaProducerTool.publishMessage("kafkamonitor-simpleproducer", message);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

  程序运行效果: 
   这里写图片描述

3、ConsoleConsumer消费该topic

  用kafka自带的ConsoleConsumer消费kafkamonitor-simpleproducer中的message。

bin/kafka-console-consumer.sh --zookeeper m000:2181 --from-beginning --topic kafkamonitor-simpleproducer
  • 1

  消费截图如下: 
   这里写图片描述

4、KafkaOffsetMonitor页面

(1)在Topic List选项卡中,我们可以看到刚才新建的 kafkamonitor-simpleproducer 
   这里写图片描述
(2)点开后,能看到有一个console-consumer正在消费该topic 
   这里写图片描述
(3)继续进入该Consumer,可以查看该Consumer当前的消费状况 
   这里写图片描述
  这张图片的左上角显示了当前Topic的生产速率,右上角显示了当前Consumer的消费速率。 
  图片中还有三种颜色的线条,蓝色的表示当前Topic中的Message数目,灰色的表示当前Consumer消费的offset位置,红色的表示蓝色灰色的差值,即当前Consumer滞后于Producer的message数目。 
(4)看一眼各partition中的message消费情况 
   这里写图片描述
  从上图可以看到,当前有3个Partition,每个Partition中的message数目分布很不均匀。这里可以与接下来的自定义Producer的情况进行一个对比。

三、自定义Partitioner的Producer

1、新建一个Topic

bin/kafka-topics.sh \
    --create \
    --zookeeper m000:2181 \
    --replication-factor 3 \
    --partition 3 \
    --topic kafkamonitor-partitionedproducer
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2、Partitioner代码

  逻辑很简单,循环依次往各Partition中发送message。

import kafka.producer.Partitioner;

/**
 * Created by ckm on 2016/8/30.
 */
public class TestPartitioner implements Partitioner {
    public TestPartitioner() {
    }

    @Override
    public int partition(Object key, int numPartitions) {
        int intKey = (int) key;
        return intKey % numPartitions;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3、Producer代码

  将自定义的Partitioner设置到Producer,其他调用过程和二中类似。

import com.ckm.kafka.producer.impl.KafkaProducerToolImpl;
import com.ckm.kafka.producer.inter.KafkaProducerTool;

/**
 * Created by ckm on 2016/8/30.
 */
public class PartitionedProducer {
    public static void main(String[] args) {
        KafkaProducerTool kafkaProducerTool = new KafkaProducerToolImpl();
        kafkaProducerTool.getProducerProperties().put("partitioner.class", "TestPartitioner");
        int i = 0;
        String message = "";
        while (true) {
            message = "test-partitioner-producer : " + i;
            System.out.println(message);
            kafkaProducerTool.publishPartitionedMessage("kafkamonitor-partitionedproducer", i + "", message);
            i ++;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

  代码运行效果如下图: 
   这里写图片描述

4、ConsoleConsumer消费Message

bin/kafka-console-consumer.sh --zookeeper m000:2181 --from-beginning --topic kafkamonitor-partitionedproducer
  • 1

  消费效果如下图: 
   这里写图片描述

5、KafkaOffsetMonitor页面

  其他页面与上面的类似,这里只观察一下每个partition中的message数目与第二节中的对比。可以看到这里每个Partition中message分别是很均匀的。 
   这里写图片描述

注意事项: 
  注意这里有一个坑,默认情况下Producer往一个不存在的Topic发送message时会自动创建这个Topic。由于在这个封装中,有同时传递message和topic的情况,如果调用方法时传入的参数反了,将会在Kafka集群中自动创建Topic。在正常情况下,应该是先把Topic根据需要创建好,然后Producer往该Topic发送Message,最好把Kafka这个默认自动创建Topic的功能关掉。 
  那么,假设真的不小心创建了多余的Topic,在删除时,会出现“marked for deletion”提示,只是将该topic标记为删除,使用list命令仍然能看到。如果需要调整这两个功能的话,在server.properties中配置如下两个参数:

参数默认值作用
auto.create.topics.enabletrueEnable auto creation of topic on the server
delete.topic.enablefalseEnables delete topic. Delete topic through the admin tool will have no effect if this config is turned off

Chaperone:来自Uber工程师团队的Kafka监控工具

$
0
0

Uber工程师团队 发布了开源项目Chaperone(中文意为监护人),这是一个 Kafka监控工具。在Uber,它被用于监控多个数据中心和大容量Kafka集群中数据丢失、延迟以及重复的问题。

Uber现在的Kafka数据管道跨越了多个数据中心。Uber的各个系统会生成大量服务调用和事件的日志信息。这已就高吞吐量进行了优化。这些服务在多个数据仓库间以多活模式运行。通过Uber的Kafka管道的数据被同时用于批处理以及实时数据分析。

Kafka作为数据总线,会连接Uber各个系统以及一个称为 uReplicator的工具。uReplicator是一个Kafka的复制器,它参照了Kafka用于复制现存集群的 MirrorMaker的原理进行设计。当日志消息被推送到Kafka的代理,代理会将消息进行汇总并推送到数据仓库对应的Kafka区域性集群。消费者会同时处理各个Kafka区域集群与合并了多个数据仓库数据的Kafka架构内的数据。Chaperone就用于实时监控这些消息。

Chaperone的首要职责是在数据通过管道时检测数据丢失、延迟、重复等数据异常。它包含四个组件:

  • 监控类库(AuditLibrary),它会收集、汇总并输出每个应用监控消息的统计信息。这个类库使用了翻转窗口(Tumbling Windows)的概念,用于汇总信息来生成监控消息,并将它们发送到对应Kafka主题(topic)。 翻转窗口常被用于像 Apache Flink这样的流处理系统中,用于将流数据分为不重叠的分片数据。
  • Chaperone服务(ChaperoneService),它会消费Kafka的每条数据并记录下时间戳,并向对应的Kafka主题中推送生成的监控消息。
  • Chaperone收集器(ChaperoneCollector),它会接收ChaperoneService产生的数据并将它们存入数据库,再将它们显示在用户界面中,这样就可以方便地检测和定位消息的丢失和延迟。
  • WebService,它会暴露出REST APIs用于获取或处理数据。

在Chaperone的实现上,必须要保证监控数据的准确性。为了实现准确性,Chaperone采用的策略是保证每一条数据会并且只会被监控一次。这里使用了预写式日志(WAL)。WAL会在消息从ChaperoneService被送到Kafka之前记录一条监控日志,这就保证了如果服务宕机,任何发送的消息都可以被重放。这个 技术常见于一些数据库,如 PostgreSQL

还有一个策略是无论监控消息是在哪里、哪一步被处理,都能使用一个一致的时间戳。Chaperone尚未完全解决这一问题。目前使用的是基于消息编码的混合技术。对于 Avro-schema编码的消息,时间戳可以在常量时间内被读出,对于JSON消息,Chaperone团队写了一个基于流的JSON解析器,它只会读取时间戳而不会解析整个JSON消息。而在代理客户端和服务端仍然使用消息处理时的时间戳。

Chaperone的作用并不仅限于检查数据丢失,还可以用其从Kafka中按照时间戳来读取数据而非通过偏移量。这样无论数据是否已经被处理,用户都可以读取任意时间范围内的数据。因此,Chaperone也可以被用作调试工具,让用户查看已经处理过的消息用以进一步分析。

Chaperone的源码可在 Github上获取。

【译】调优Apache Kafka集群 - huxihx - 博客园

$
0
0

  今天带来一篇译文“调优Apache Kafka集群”,里面有一些观点并无太多新颖之处,但总结得还算详细。该文从四个不同的目标出发给出了各自不同的参数配置,值得大家一读~ 原文地址请参考:https://www.confluent.io/blog/optimizing-apache-kafka-deployment/

==========================================

  Apache Kafka是当前最好的企业级流式处理平台。把你的应用程序链接到Kafka集群,剩下的所有事情Kafka都可以帮你做了:自动帮你完成负载均衡,自动实现Zero-Copy的数据传输、消费者组成员变动时自动的rebalance以及应用状态持久化存储的自动备份以及分区leader自动的故障转移等——运维人员的梦想终于成真了!

————笔者:最近在看Apache Flink。说到streaming这部分,Flink可一点都不比Kafka streams差。至于是不是最好的流式处理平台,仁者见仁吧~~

  使用默认的Kafka参数配置你就能够从零搭建起一个Kafka集群环境用于开发及测试之用,但默认配置通常都不匹配你的生产环境,因此必须要做某种程度的调优。毕竟不同的使用场景有着不同的使用需求和性能指标。而Kafka提供的各种参数就是为了优化这些需求和指标的。Kafka提供了很多配置供用户设置以确保搭建起来的Kafka环境是能够满足需求目标的,因此详细地去调研这些参数的含义以及针对不同参数值进行测试是非常重要的。所有这些工作都应该在Kafka正式上生产环境前就做好,并且各种参数的配置要考虑未来集群规模的扩展。

   执行优化的流程如下图所示:

  1. 明确调优目标
  2. 有针对性地配置Kafka server端和clients端参数
  3. 执行性能测试,监控各个指标以确定是否满足需求以及是否有进一步调优的可能

一、确立目标 

  第一步就是要明确性能调优目标,主要从4个方面考虑:吞吐量(throughput)、延时(latency)、持久性(durability)和可用性(availability)。根据实际的使用场景来确定要达到这4个中的哪个(或哪几个)目标。有时候我们可能很难确定自己到底想要什么,那么此时可以尝试采用这样的方法:让你的团队坐下来讨论一下原本的业务使用场景然后看看主要的业务目标是什么。确立目标的原因主要有两点:

  • “鱼和熊掌不可兼得”——你没有办法最大化所有目标。这4者之间必然存在着权衡(tradeoff)。常见的tradeoff包括:吞吐量和延时权衡、持久性和可用性之间权衡。但是当我们考虑整个系统时通常都不能孤立地只考虑其中的某一个方面,而是需要全盘考量。虽然它们之间不是互斥的,但使所有目标同时达到最优几乎肯定是不可能的
  • 我们需要不断调整Kafka配置参数以实现这些目标,并确保我们对Kafka的优化是满足用户实际使用场景的需要

  下面的这些问题可以帮助你确立目标:

  • 是否期望着Kafka实现高吞吐量(TPS,即producer生产速度和consumer消费速度),比如几百万的TPS?由于Kafka自身良好的设计,生产超大数量的消息并不是什么难事。比起传统的数据库或KV存储而言,Kafka要快得多,而且使用普通的硬件就能够做到这点
  • 是否期望着Kafka实现低延时(即消息从被写入到被读取之间的时间间隔越小越好)? 低延时的一个实际应用场景就是平时的聊天程序,接收到某一条消息越快越好。其他的例子还包括交互性网站中用户期望实时看到好友动态以及物联网中的实时流处理等
  • 是否期望着Kafka实现高持久性,即被成功提交的消息永远不能丢失?比如事件驱动的微服务数据管道使用Kafka作为底层数据存储,那么就要求Kafka不能丢失事件。再比如streaming框架读取持久化存储时一定要确保关键的业务事件不能遗漏等
  • 是否期望着Kafka实现高可用?即使出现崩溃也不能出现服务的整体宕机。Kafka本身是分布式系统,天然就是能够对抗崩溃的。如果高可用是你的主要目标,配置特定的参数确保Kafka可以及时从崩溃中恢复就显得至关重要了

二、配置参数

下面我们将分别讨论这四个目标的优化以及对应的参数设置。这些参数涵盖了producer端、broker端和consumer端的不同配置。如前所述,很多配置都提现了某种程度的tradeoff,在使用时一定要弄清楚这些配置的真正含义,做到有的放矢。

producer端

  • batch.size
  • linger.ms
  • compression.type
  • acks
  • retries
  • max.in.flight.requests.per.connection
  • buffer.memory

Broker端

  • default.replication.factor
  • num.replica.fetchers
  • auto.create.topics.enable
  • min.insync.replicas
  • unclean.leader.election.enable
  • broker.rack
  • log.flush.interval.messages
  • log.flush.interval.ms
  • unclean.leader.election.enable
  • min.insync.replicas
  • num.recovery.threads.per.data.dir

Consumer端

  • fetch.min.bytes
  • auto.commit.enable
  • session.timeout.ms

1 调优吞吐量

Producer端

  • batch.size = 100000 - 200000(默认是16384,通常都太小了)
  • linger.ms = 10 - 100 (默认是0)
  • compression.type = lz4
  • acks = 1
  • retries = 0
  • buffer.memory:如果分区数很多则适当增加 (默认是32MB)

Consumer端

  • fetch.min.bytes = 10 ~ 100000 (默认是1)

2 调优延时

Producer端

  • linger.ms = 0
  • compression.type = none
  • acks = 1

Broker端

  • num.replica.fetchers:如果发生ISR频繁进出的情况或follower无法追上leader的情况则适当增加该值,但通常不要超过CPU核数+1

Consumer端

  • fetch.min.bytes = 1

3 调优持久性

Producer端

  • replication.factor = 3
  • acks = all
  • retries = 相对较大的值,比如5 ~ 10
  • max.in.flight.requests.per.connection = 1 (防止乱序)

Broker端

  • default.replication.factor = 3
  • auto.create.topics.enable = false
  • min.insync.replicas = 2,即设置为replication factor - 1
  • unclean.leader.election.enable = false
  • broker.rack: 如果有机架信息,则最好设置该值,保证数据在多个rack间的分布性以达到高持久化
  • log.flush.interval.messages和log.flush.interval.ms: 如果是特别重要的topic并且TPS本身也不高,则推荐设置成比较低的值,比如1

Consumer端

  • auto.commit.enable = false 自己控制位移

4 调优高可用

 

Broker端

  • unclean.leader.election.enable = true
  • min.insync.replicas = 1
  • num.recovery.threads.per.data.dir = log.dirs中配置的目录数

Consumer端

  • session.timeout.ms:尽可能地低

三、指标监控

1 操作系统级指标

  • 内存使用率
  • 磁盘占用率
  • CPU使用率
  • 打开的文件句柄数
  • 磁盘IO使用率
  • 带宽IO使用率

2 Kafka常规JMX监控

3 易发现瓶颈的JMX监控

 

4 clients端常用JMX监控 

 

5 broker端ISR相关的JMX监控 

==========================================

  以上就是这篇原文的简要译文。还是那句话,里面的很多参数设置都已经司空见惯了,并无太多新意。不过这篇文章从吞吐量、延时、持久化和可用性4个方面给出了不同的思考。从这一点上来说还是值得一读的。

Kafka 最佳实践【译】 | Matt's Blog

$
0
0

这里翻译一篇关于 Kafka 实践的文章,内容来自 DataWorks Summit/Hadoop Summit( Hadoop Summit)上一篇分享,PPT 见 Apache Kafka Best Pratices,里面讲述了很多关于 Kafka 配置、监控、优化的内容,绝对是在实践中总结出的精华,有很大的借鉴参考意义,本文主要是根据 PPT 的内容进行翻译及适当补充。

Kafka 的架构这里就不多做介绍了,直接不如正题。

Kafka 基本配置及性能优化

这里主要是 Kafka 集群基本配置的相关内容。

硬件要求

Kafka 集群基本硬件的保证

集群规模内存CPU存储
Kafka Brokers3+24GB+(小规模);64GB+(大规模)多核(12CPU+),并允许超线程6+ 1TB 的专属磁盘(RAID 或 JBOD)
Zookeeper3(小规模);5(大规模)8GB+(小规模);24GB+(大规模)2核+SSD 用于中间的日志传输

OS 调优

  • OS page cache:应当可以缓存所有活跃的 Segment(Kafka 中最基本的数据存储单位);
  • fd 限制:100k+;
  • 禁用 swapping:简单来说,swap 作用是当内存的使用达到一个临界值时就会将内存中的数据移动到 swap 交换空间,但是此时,内存可能还有很多空余资源,swap 走的是磁盘 IO,对于内存读写很在意的系统,最好禁止使用 swap 分区(参考 What is swapping in an OS?);
  • TCP 调优;
  • JVM 配置
    1. JDK 8 并且使用 G1 垃圾收集器;
    2. 至少要分配 6-8 GB 的堆内存。

Kafka 磁盘存储

  • 使用多块磁盘,并配置为 Kafka 专用的磁盘;
  • JBOD vs RAID10;
  • JBOD(Just a Bunch of Disks,简单来说它表示一个没有控制软件提供协调控制的磁盘集合,它将多个物理磁盘串联起来,提供一个巨大的逻辑磁盘,数据是按序存储,它的性能与单块磁盘类似)
  • JBOD 的一些缺陷:
    • 任何磁盘的损坏都会导致异常关闭,并且需要较长的时间恢复;
    • 数据不保证一致性;
    • 多级目录;
  • 社区也正在解决这么问题,可以关注 KIP 112、113:
    • 必要的工具用于管理 JBOD;
    • 自动化的分区管理;
    • 磁盘损坏时,Broker 可以将 replicas 迁移到好的磁盘上;
    • 在同一个 Broker 的磁盘间 reassign replicas;
  • RAID 10 的特点:
    • 可以允许单磁盘的损坏;
    • 性能和保护;
    • 不同磁盘间的负载均衡;
    • 高命中来减少 space;
    • 单一的 mount point;
  • 文件系统:
    • 使用 EXT 或 XFS;
    • SSD;

基本的监控

Kafka 集群需要监控的一些指标,这些指标反应了集群的健康度。

  • CPU 负载;
  • Network Metrics;
  • File Handle 使用;
  • 磁盘空间;
  • 磁盘 IO 性能;
  • GC 信息;
  • ZooKeeper 监控。

Kafka replica 相关配置及监控

Kafka Replication

  • Partition 有两种副本:Leader,Follower;
  • Leader 负责维护 in-sync-replicas(ISR)
    • replica.lag.time.max.ms:默认为10000,如果 follower 落后于 leader 的消息数超过这个数值时,leader 就将 follower 从 isr 列表中移除;
    • num.replica.fetchers,默认为1,用于从 leader 同步数据的 fetcher 线程数;
    • min.insync.replica:Producer 端使用来用于保证 Durability(持久性);

Under Replicated Partitions

当发现 replica 的配置与集群的不同时,一般情况都是集群上的 replica 少于配置数时,可以从以下几个角度来排查问题:

  • JMX 监控项:kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions;
  • 可能的原因:
    • Broker 挂了?
    • Controller 的问题?
    • ZooKeeper 的问题?
    • Network 的问题?
  • 解决办法:
    • 调整 ISR 的设置;
    • Broker 扩容。

Controller

  • 负责管理 partition 生命周期;
  • 避免 Controller’s ZK 会话超时:
    • ISR 抖动;
    • ZK Server 性能问题;
    • Broker 长时间的 GC;
    • 网络 IO 问题;
  • 监控:
    • kafka.controller:type=KafkaController,name=ActiveControllerCount,应该为1;
    • LeaderElectionRate。

Unclean leader 选举

允许不在 isr 中 replica 被选举为 leader。

  • 这是 Availability 和 Correctness 之间选择,Kafka 默认选择了可用性;
  • unclean.leader.election.enable:默认为 true,即允许不在 isr 中 replica 选为 leader,这个配置可以全局配置,也可以在 topic 级别配置;
  • 监控:kafka.controller:type=ControllerStats,name=UncleanLeaderElectionsPerSec。

Broker 配置

Broker 级别有几个比较重要的配置,一般需要根据实际情况进行相应配置的:

  • log.retention.{ms, minutes, hours}, log.retention.bytes:数据保存时间;
  • message.max.bytes, replica.fetch.max.bytes
  • delete.topic.enable:默认为 false,是否允许通过 admin tool 来删除 topic;
  • unclean.leader.election.enable= false,参见上面;
  • min.insync.replicas= 2:当 Producer 的 acks 设置为 all 或 -1 时, min.insync.replicas代表了必须进行确认的最小 replica 数,如果不够的话 Producer 将会报 NotEnoughReplicasNotEnoughReplicasAfterAppend异常;
  • replica.lag.time.max.ms(超过这个时间没有发送请求的话,follower 将从 isr 中移除), num.replica.fetchers;
  • replica.fetch.response.max.bytes
  • zookeeper.session.timeout.ms= 30s;
  • num.io.threads:默认为8,KafkaRequestHandlerPool 的大小。

Kafka 相关资源的评估

集群评估

  • Broker 评估
    • 每个 Broker 的 Partition 数不应该超过2k;
    • 控制 partition 大小(不要超过25GB);
  • 集群评估(Broker 的数量根据以下条件配置)
    • 数据保留时间;
    • 集群的流量大小;
  • 集群扩容:
    • 磁盘使用率应该在 60% 以下;
    • 网络使用率应该在 75% 以下;
  • 集群监控
    • 保持负载均衡;
    • 确保 topic 的 partition 均匀分布在所有 Broker 上;
    • 确保集群的阶段没有耗尽磁盘或带宽。

Broker 监控

  • Partition 数:kafka.server:type=ReplicaManager,name=PartitionCount;
  • Leader 副本数:kafka.server:type=ReplicaManager,name=LeaderCount;
  • ISR 扩容/缩容率:kafka.server:type=ReplicaManager,name=IsrExpandsPerSec;
  • 读写速率:Message in rate/Byte in rate/Byte out rate;
  • 网络请求的平均空闲率:NetworkProcessorAvgIdlePercent;
  • 请求处理平均空闲率:RequestHandlerAvgIdlePercent。

Topic 评估

  • partition 数
    • Partition 数应该至少与最大 consumer group 中 consumer 线程数一致;
    • 对于使用频繁的 topic,应该设置更多的 partition;
    • 控制 partition 的大小(25GB 左右);
    • 考虑应用未来的增长(可以使用一种机制进行自动扩容);
  • 使用带 key 的 topic;
  • partition 扩容:当 partition 的数据量超过一个阈值时应该自动扩容(实际上还应该考虑网络流量)。

合理地设置 partition

  • 根据吞吐量的要求设置 partition 数:
    • 假设 Producer 单 partition 的吞吐量为 P;
    • consumer 消费一个 partition 的吞吐量为 C;
    • 而要求的吞吐量为 T;
    • 那么 partition 数至少应该大于 T/P、T/c 的最大值;
  • 更多的 partition,意味着:
    • 更多的 fd;
    • 可能增加 Unavailability(可能会增加不可用的时间);
    • 可能增加端到端的延迟;
    • client 端将会使用更多的内存。

关于 Partition 的设置可以参考这篇文章 How to choose the number of topics/partitions in a Kafka cluster?,这里简单讲述一下,Partition 的增加将会带来以下几个优点和缺点:

  1. 增加吞吐量:对于 consumer 来说,一个 partition 只能被一个 consumer 线程所消费,适当增加 partition 数,可以增加 consumer 的并发,进而增加系统的吞吐量;
  2. 需要更多的 fd:对于每一个 segment,在 broker 都会有一个对应的 index 和实际数据文件,而对于 Kafka Broker,它将会对于每个 segment 每个 index 和数据文件都会打开相应的 file handle(可以理解为 fd),因此,partition 越多,将会带来更多的 fd;
  3. 可能会增加数据不可用性(主要是指增加不可用时间):主要是指 broker 宕机的情况,越多的 partition 将会意味着越多的 partition 需要 leader 选举(leader 在宕机这台 broker 的 partition 需要重新选举),特别是如果刚好 controller 宕机,重新选举的 controller 将会首先读取所有 partition 的 metadata,然后才进行相应的 leader 选举,这将会带来更大不可用时间;
  4. 可能增加 End-to-end 延迟:一条消息只有其被同步到 isr 的所有 broker 上后,才能被消费,partition 越多,不同节点之间同步就越多,这可能会带来毫秒级甚至数十毫秒级的延迟;
  5. Client 将会需要更多的内存:Producer 和 Consumer 都会按照 partition 去缓存数据,每个 partition 都会带来数十 KB 的消耗,partition 越多, Client 将会占用更多的内存。

Producer 的相关配置、性能调优及监控

Quotas

  • 避免被恶意 Client 攻击,保证 SLA;
  • 设置 produce 和 fetch 请求的字节速率阈值;
  • 可以应用在 user、client-id、或者 user 和 client-id groups;
  • Broker 端的 metrics 监控:throttle-rate、byte-rate;
  • replica.fetch.response.max.bytes:用于限制 replica 拉取请求的内存使用;
  • 进行数据迁移时限制贷款的使用, kafka-reassign-partitions.sh -- -throttle option

Kafka Producer

  • 使用 Java 版的 Client;
  • 使用 kafka-producer-perf-test.sh测试你的环境;
  • 设置内存、CPU、batch 压缩;
    • batch.size:该值设置越大,吞吐越大,但延迟也会越大;
    • linger.ms:表示 batch 的超时时间,该值越大,吞吐越大、但延迟也会越大;
    • max.in.flight.requests.per.connection:默认为5,表示 client 在 blocking 之前向单个连接(broker)发送的未确认请求的最大数,超过1时,将会影响数据的顺序性;
    • compression.type:压缩设置,会提高吞吐量;
    • acks:数据 durability 的设置;
  • 避免大消息
    • 会使用更多的内存;
    • 降低 Broker 的处理速度;

性能调优

  • 如果吞吐量小于网络带宽
    • 增加线程;
    • 提高 batch.size;
    • 增加更多 producer 实例;
    • 增加 partition 数;
  • 设置 acks=-1 时,如果延迟增大:可以增大 num.replica.fetchers(follower 同步数据的线程数)来调解;
  • 跨数据中心的传输:增加 socket 缓冲区设置以及 OS tcp 缓冲区设置。

Prodcuer 监控

  • batch-size-avg
  • compression-rate-avg
  • waiting-threads
  • buffer-available-bytes
  • record-queue-time-max
  • record-send-rate
  • records-per-request-avg

Kafka Consumer 配置、性能调优及监控

Kafka Consumer

  • 使用 kafka-consumer-perf-test.sh测试环境;
  • 吞吐量问题:
    • partition 数太少;
    • OS page cache:分配足够的内存来缓存数据;
    • 应用的处理逻辑;
  • offset topic( __consumer_offsets
    • offsets.topic.replication.factor:默认为3;
    • offsets.retention.minutes:默认为1440,即 1day;
      – MonitorISR,topicsize;
  • offset commit较慢:异步 commit 或 手动 commit。

Consumer 配置

  • fetch.min.bytesfetch.max.wait.ms
  • max.poll.interval.ms:调用 poll()之后延迟的最大时间,超过这个时间没有调用 poll()的话,就会认为这个 consumer 挂掉了,将会进行 rebalance;
  • max.poll.records:当调用 poll()之后返回最大的 record 数,默认为500;
  • session.timeout.ms
  • Consumer Rebalance
    – check timeouts
    – check processing times/logic
    – GC Issues
  • 网络配置;

Consumer 监控

consumer 是否跟得上数据的发送速度。

  • Consumer Lag:consumer offset 与 the end of log(partition 可以消费的最大 offset) 的差值;
  • 监控
    • metric 监控:records-lag-max;
    • 通过 bin/kafka-consumer-groups.sh查看;
    • 用于 consumer 监控的 LinkedIn’s Burrow;
  • 减少 Lag
    • 分析 consumer:是 GC 问题还是 Consumer hang 住了;
    • 增加 Consumer 的线程;
    • 增加分区数和 consumer 线程;

如何保证数据不丢

这个是常用的配置,这里截了 PPT 中的内容

Kafka 数据不丢配置

  • block.on.buffer.full:默认设置为 false,当达到内存设置时,可能通过 block 停止接受新的 record 或者抛出一些错误,默认情况下,Producer 将不会抛出 BufferExhaustException,而是当达到 max.block.ms这个时间后直接抛出 TimeoutException。设置为 true 的意义就是将 max.block.ms设置为 Long.MAX_VALUE,未来版本中这个设置将被遗弃,推荐设置 max.block.ms

参考:

  1. Apache Kafka Best Pratices
  2. 胡夕- 【译】Kafka最佳实践 / Kafka Best Practices
  3. How to choose the number of topics/partitions in a Kafka cluster?
  4. raid有哪几种有什么区别?希望讲通俗点
  5. File Descriptors and File Handles (and C).

记一次kafka数据丢失问题的排查 - CSDN博客

$
0
0
数据丢失为大事,针对数据丢失的问题我们排查结果如下。
第一:是否存在数据丢失的问题?
    存在,且已重现。

第二:是在什么地方丢失的数据,是否是YDB的问题?
    数据丢失是在导入阶段,数据并没有写入到Kafka里面,所以YDB也就不会从Kafka里面消费到缺失的数据,数据丢失与延云YDB无关。

第三:是如何发现有数据丢失?
    1.测试数据会一共创建365个分区,每个分区均是9亿数据,如果最终每个分区还是9亿(多一条少一条均不行),则数据完整。
    2.测试开始第二天,开始有丢失数据的现象,且丢失的数据越来越多。

第四:如何定位到是写入端丢失数据的,而不是YDB消费丢失数据的?
    kafka支持数据的重新回放的功能(换个消费group),我们清空了ydb的所有数据,重新用kafka回放了原先的数据。
    如果是在ydb消费端丢失数据,那么第二遍回放数据的结果,跟第一次消费的数据在条数上肯定会有区别,完全一模一样的几率很低。
    数据回放结果为:与第一次回放结果完全一样,可以确认为写入段丢失。

第五:写入kafka数据为什么会丢失?
    导入数据我们采用的为kafka给的 官方的默认示例,官方默认并没有处理网络负载很高或者磁盘很忙写入失败的情况(网上遇到同类问题的也很多)
    一旦网络中断或者磁盘负载很高导致的写入失败,并没有自动重试重发消息。
    而我们之前的测试,
    第1次测试是在共享集群环境上做的测试,由于有其他任务的影响,网络与负载很不稳定,就会导致数据丢失。
    第2次测试是在独立集群,并没有其他任务干预,但是我们导入程序与kafka不在一台机器上,而我们又没有做限速处理(每小时导入5亿条数据)
    千兆网卡的流量常态在600~800M左右,如果此时突然又索引合并,瞬间的网络跑满是很正常的,丢包也是很正常的。
    延云之前持续压了20多天,确实一条数据没有丢失,究其原因是导入程序与kafka在同一个机器上,且启用了限速。

第六:这个问题如何解决?
    官方给出的默认示例并不可靠,并没有考虑到网络繁忙的情况,并不适合生产。
    故kafka一定要配置上消息重试的机制,并且重试的时间间隔一定要长一些,默认1秒钟并不符合生产环境(网络中断时间有可能超过1秒)。
    延云认为,增加如下参数会较大幅度的减少kafka写入数据照成的数据丢失,在公司实测,目前还没遇到数据丢失的情况。
         props.put("compression.type", "gzip");
         props.put("linger.ms", "50");
         props.put("acks", "all");
         props.put("retries ", 30);
         props.put("reconnect.backoff.ms ", 20000);
         props.put("retry.backoff.ms", 20000);
    


Avoiding Data Loss - 避免Kafka数据丢失

$
0
0

If for some reason the producer cannot deliver messages that have been consumed and committed by the consumer, it is possible for a MirrorMaker process to lose data.

To prevent data loss, use the following settings. (Note: these are the default settings.)

  • For consumers:

    • auto.commit.enabled=false

  • For producers:

    • max.in.flight.requests.per.connection=1

    • retries=Int.MaxValue

    • acks=-1

    • block.on.buffer.full=true

  • Specify the  --abortOnSendFail option to MirrorMaker

The following actions will be taken by MirrorMaker:

  • MirrorMaker will send only one request to a broker at any given point.

  • If any exception is caught in the MirrorMaker thread, MirrorMaker will try to commit the acked offsets and then exit immediately.

  • On a  RetriableException in the producer, the producer will retry indefinitely. If the retry does not work, MirrorMaker will eventually halt when the producer buffer is full.

  • On a non-retriable exception, if  --abort.on.send.fail is specified, MirrorMaker will stop.

    If  --abort.on.send.fail is not specified, the producer callback mechanism will record the message that was not sent, and MirrorMaker will continue running. In this case, the message will not be replicated in the target cluster.

Kafka无消息丢失配置 - huxihx - 博客园

$
0
0

Kafka到底会不会丢数据(data loss)? 通常不会,但有些情况下的确有可能会发生。下面的参数配置及Best practice列表可以较好地保证数据的持久性(当然是trade-off,牺牲了吞吐量)。笔者会在该列表之后对列表中的每一项进行讨论,有兴趣的同学可以看下后面的分析。

  1. block.on.buffer.full = true
  2. acks = all
  3. retries = MAX_VALUE
  4. max.in.flight.requests.per.connection = 1
  5. 使用KafkaProducer.send(record, callback)
  6. callback逻辑中显式关闭producer:close(0) 
  7. unclean.leader.election.enable=false
  8. replication.factor = 3 
  9. min.insync.replicas = 2
  10. replication.factor > min.insync.replicas
  11. enable.auto.commit=false
  12. 消息处理完成之后再提交位移

给出列表之后,我们从两个方面来探讨一下数据为什么会丢失:

1. Producer端

  目前比较新版本的Kafka正式替换了Scala版本的old producer,使用了由Java重写的producer。新版本的producer采用异步发送机制。KafkaProducer.send(ProducerRecord)方法仅仅是把这条消息放入一个缓存中(即RecordAccumulator,本质上使用了队列来缓存记录),同时后台的IO线程会不断扫描该缓存区,将满足条件的消息封装到某个batch中然后发送出去。显然,这个过程中就有一个数据丢失的窗口:若IO线程发送之前client端挂掉了,累积在accumulator中的数据的确有可能会丢失。

  Producer的另一个问题是消息的乱序问题。假设客户端代码依次执行下面的语句将两条消息发到相同的分区

producer.send(record1);
producer.send(record2);

如果此时由于某些原因(比如瞬时的网络抖动)导致record1没有成功发送,同时Kafka又配置了重试机制和max.in.flight.requests.per.connection大于1(默认值是5,本来就是大于1的),那么重试record1成功后,record1在分区中就在record2之后,从而造成消息的乱序。很多某些要求强顺序保证的场景是不允许出现这种情况的。

  鉴于producer的这两个问题,我们应该如何规避呢??对于消息丢失的问题,很容易想到的一个方案就是:既然异步发送有可能丢失数据, 我改成同步发送总可以吧?比如这样:

producer.send(record).get();

这样当然是可以的,但是性能会很差,不建议这样使用。因此特意总结了一份配置列表。个人认为该配置清单应该能够比较好地规避producer端数据丢失情况的发生:(特此说明一下,软件配置的很多决策都是trade-off,下面的配置也不例外:应用了这些配置,你可能会发现你的producer/consumer 吞吐量会下降,这是正常的,因为你换取了更高的数据安全性)

  • block.on.buffer.full = true  尽管该参数在0.9.0.0已经被标记为“deprecated”,但鉴于它的含义非常直观,所以这里还是显式设置它为true,使得producer将一直等待缓冲区直至其变为可用。否则如果producer生产速度过快耗尽了缓冲区,producer将抛出异常
  • acks=all  很好理解,所有follower都响应了才认为消息提交成功,即"committed"
  • retries = MAX 无限重试,直到你意识到出现了问题:)
  • max.in.flight.requests.per.connection = 1 限制客户端在单个连接上能够发送的未响应请求的个数。设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求。注意:设置此参数是为了避免消息乱序
  • 使用KafkaProducer.send(record, callback)而不是send(record)方法   自定义回调逻辑处理消息发送失败
  • callback逻辑中最好显式关闭producer:close(0) 注意:设置此参数是为了避免消息乱序
  • unclean.leader.election.enable=false   关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失
  • replication.factor >= 3   这个完全是个人建议了,参考了Hadoop及业界通用的三备份原则
  • min.insync.replicas > 1 消息至少要被写入到这么多副本才算成功,也是提升数据持久性的一个参数。与acks配合使用
  • 保证replication.factor > min.insync.replicas  如果两者相等,当一个副本挂掉了分区也就没法正常工作了。通常设置replication.factor = min.insync.replicas + 1即可

2. Consumer端

  consumer端丢失消息的情形比较简单:如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。由于Kafka consumer默认是自动提交位移的,所以在后台提交位移前一定要保证消息被正常处理了,因此不建议采用很重的处理逻辑,如果处理耗时很长,则建议把逻辑放到另一个线程中去做。为了避免数据丢失,现给出两点建议:

  • enable.auto.commit=false  关闭自动提交位移
  • 在消息被完整处理之后再手动提交位移

Redis 高负载下的中断优化

$
0
0

背景

2017年年初以来,随着Redis产品的用户量越来越大,接入服务越来越多,再加上美团点评Memcache和Redis两套缓存融合,Redis服务端的总体请求量从年初最开始日访问量百亿次级别上涨到高峰时段的万亿次级别,给运维和架构团队都带来了极大的挑战。

原本稳定的环境也因为请求量的上涨带来了很多不稳定的因素,其中一直困扰我们的就是网卡丢包问题。起初线上存在部分Redis节点还在使用千兆网卡的老旧服务器,而缓存服务往往需要承载极高的查询量,并要求毫秒级的响应速度,如此一来千兆网卡很快就出现了瓶颈。经过整治,我们将千兆网卡服务器替换为了万兆网卡服务器,本以为可以高枕无忧,但是没想到,在业务高峰时段,机器也竟然出现了丢包问题,而此时网卡带宽使用还远远没有达到瓶颈。

定位网络丢包的原因

从异常指标入手

首先,我们在系统监控的net.if.in.dropped指标中,看到有大量数据丢包异常,那么第一步就是要了解这个指标代表什么。

这个指标的数据源,是读取/proc/net/dev中的数据,监控Agent做简单的处理之后上报。以下为/proc/net/dev 的一个示例,可以看到第一行Receive代表in,Transmit代表out,第二行即各个表头字段,再往后每一行代表一个网卡设备具体的值。

其中各个字段意义如下:

字段解释
bytesThe total number of bytes of data transmitted or received by the interface.
packetsThe total number of packets of data transmitted or received by the interface.
errsThe total number of transmit or receive errors detected by the device driver.
dropThe total number of packets dropped by the device driver.
fifoThe number of FIFO buffer errors.
frameThe number of packet framing errors.
collsThe number of collisions detected on the interface.
compressedThe number of compressed packets transmitted or received by the device driver. (This appears to be unused in the 2.2.15 kernel.)
carrierThe number of carrier losses detected by the device driver.
multicastThe number of multicast frames transmitted or received by the device driver.

通过上述字段解释,我们可以了解丢包发生在网卡设备驱动层面;但是想要了解真正的原因,需要继续深入源码。

/proc/net/dev的数据来源,根据源码文件net/core/net-procfs.c,可以知道上述指标是通过其中的dev_seq_show()函数和dev_seq_printf_stats()函数输出的:

static int dev_seq_show(struct seq_file *seq, void *v)
{
    if (v == SEQ_START_TOKEN)
        /* 输出/proc/net/dev表头部分   */
        seq_puts(seq, "Inter-|   Receive                            ""                    |  Transmit\n"" face |bytes    packets errs drop fifo frame ""compressed multicast|bytes    packets errs ""drop fifo colls carrier compressed\n");
    else
        /* 输出/proc/net/dev数据部分   */
        dev_seq_printf_stats(seq, v);
    return 0;
}

static void dev_seq_printf_stats(struct seq_file *seq, struct net_device *dev)
{
    struct rtnl_link_stats64 temp;

    /* 数据源从下面的函数中取得   */
    const struct rtnl_link_stats64 *stats = dev_get_stats(dev, &temp);

    /* /proc/net/dev 各个字段的数据算法   */
    seq_printf(seq, "%6s: %7llu %7llu %4llu %4llu %4llu %5llu %10llu %9llu ""%8llu %7llu %4llu %4llu %4llu %5llu %7llu %10llu\n",
           dev->name, stats->rx_bytes, stats->rx_packets,
           stats->rx_errors,
           stats->rx_dropped + stats->rx_missed_errors,
           stats->rx_fifo_errors,
           stats->rx_length_errors + stats->rx_over_errors +
            stats->rx_crc_errors + stats->rx_frame_errors,
           stats->rx_compressed, stats->multicast,
           stats->tx_bytes, stats->tx_packets,
           stats->tx_errors, stats->tx_dropped,
           stats->tx_fifo_errors, stats->collisions,
           stats->tx_carrier_errors +
            stats->tx_aborted_errors +
            stats->tx_window_errors +
            stats->tx_heartbeat_errors,
           stats->tx_compressed);
}

dev_seq_printf_stats()函数里,对应drop输出的部分,能看到由两块组成:stats->rx_dropped+stats->rx_missed_errors。

继续查找dev_get_stats函数可知,rx_dropped和rx_missed_errors 都是从设备获取的,并且需要设备驱动实现。

/**
 *  dev_get_stats   - get network device statistics
 *  @dev: device to get statistics from
 *  @storage: place to store stats
 *
 *  Get network statistics from device. Return @storage.
 *  The device driver may provide its own method by setting
 *  dev->netdev_ops->get_stats64 or dev->netdev_ops->get_stats;
 *  otherwise the internal statistics structure is used.
 */
struct rtnl_link_stats64 *dev_get_stats(struct net_device *dev,
                    struct rtnl_link_stats64 *storage)
{
    const struct net_device_ops *ops = dev->netdev_ops;
    if (ops->ndo_get_stats64) {
        memset(storage, 0, sizeof(*storage));
        ops->ndo_get_stats64(dev, storage);
    } else if (ops->ndo_get_stats) {
        netdev_stats_to_stats64(storage, ops->ndo_get_stats(dev));
    } else {
        netdev_stats_to_stats64(storage, &dev->stats);
    }   
    storage->rx_dropped += (unsigned long)atomic_long_read(&dev->rx_dropped);
    storage->tx_dropped += (unsigned long)atomic_long_read(&dev->tx_dropped);
    storage->rx_nohandler += (unsigned long)atomic_long_read(&dev->rx_nohandler);
    return storage;
}

结构体 rtnl_link_stats64 的定义在 /usr/include/linux/if_link.h 中:

/* The main device statistics structure */
struct rtnl_link_stats64 {
    __u64   rx_packets;     /* total packets received   */
    __u64   tx_packets;     /* total packets transmitted    */
    __u64   rx_bytes;       /* total bytes received     */
    __u64   tx_bytes;       /* total bytes transmitted  */
    __u64   rx_errors;      /* bad packets received     */
    __u64   tx_errors;      /* packet transmit problems */
    __u64   rx_dropped;     /* no space in linux buffers    */
    __u64   tx_dropped;     /* no space available in linux  */
    __u64   multicast;      /* multicast packets received   */
    __u64   collisions;

    /* detailed rx_errors: */
    __u64   rx_length_errors;
    __u64   rx_over_errors;     /* receiver ring buff overflow  */
    __u64   rx_crc_errors;      /* recved pkt with crc error    */
    __u64   rx_frame_errors;    /* recv'd frame alignment error */
    __u64   rx_fifo_errors;     /* recv'r fifo overrun      */
    __u64   rx_missed_errors;   /* receiver missed packet   */

    /* detailed tx_errors */
    __u64   tx_aborted_errors;
    __u64   tx_carrier_errors;
    __u64   tx_fifo_errors;
    __u64   tx_heartbeat_errors;
    __u64   tx_window_errors;

    /* for cslip etc */
    __u64   rx_compressed;
    __u64   tx_compressed;
};

至此,我们知道rx_dropped是Linux中的缓冲区空间不足导致的丢包,而rx_missed_errors则在注释中写的比较笼统。有资料指出,rx_missed_errors是fifo队列(即rx ring buffer)满而丢弃的数量,但这样的话也就和rx_fifo_errors等同了。后来公司内网络内核研发大牛王伟给了我们点拨:不同网卡自己实现不一样,比如Intel的igb网卡rx_fifo_errors在missed的基础上,还加上了RQDPC计数,而ixgbe就没这个统计。RQDPC计数是描述符不够的计数,missed是fifo满的计数。所以对于ixgbe来说,rx_fifo_errors和rx_missed_errors确实是等同的。

通过命令ethtool -S eth0可以查看网卡一些统计信息,其中就包含了上文提到的几个重要指标rx_dropped、rx_missed_errors、rx_fifo_errors等。但实际测试后,我发现不同网卡型号给出的指标略有不同,比如Intel ixgbe就能取到,而Broadcom bnx2/tg3则只能取到rx_discards(对应rx_fifo_errors)、rx_fw_discards(对应rx_dropped)。这表明,各家网卡厂商设备内部对这些丢包的计数器、指标的定义略有不同,但通过驱动向内核提供的统计数据都封装成了struct rtnl_link_stats64定义的格式。

在对丢包服务器进行检查后,发现rx_missed_errors为0,丢包全部来自rx_dropped。说明丢包发生在Linux内核的缓冲区中。接下来,我们要继续探索到底是什么缓冲区引起了丢包问题,这就需要完整地了解服务器接收数据包的过程。

了解接收数据包的流程

接收数据包是一个复杂的过程,涉及很多底层的技术细节,但大致需要以下几个步骤:

  1. 网卡收到数据包。
  2. 将数据包从网卡硬件缓存转移到服务器内存中。
  3. 通知内核处理。
  4. 经过TCP/IP协议逐层处理。
  5. 应用程序通过read()从socket buffer读取数据。

将网卡收到的数据包转移到主机内存(NIC与驱动交互)

NIC在接收到数据包之后,首先需要将数据同步到内核中,这中间的桥梁是rx ring buffer。它是由NIC和驱动程序共享的一片区域,事实上,rx ring buffer存储的并不是实际的packet数据,而是一个描述符,这个描述符指向了它真正的存储地址,具体流程如下:

  1. 驱动在内存中分配一片缓冲区用来接收数据包,叫做sk_buffer;
  2. 将上述缓冲区的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的缓冲区地址是DMA使用的物理地址;
  3. 驱动通知网卡有一个新的描述符;
  4. 网卡从rx ring buffer中取出描述符,从而获知缓冲区的地址和大小;
  5. 网卡收到新的数据包;
  6. 网卡将新数据包通过DMA直接写到sk_buffer中。

当驱动处理速度跟不上网卡收包速度时,驱动来不及分配缓冲区,NIC接收到的数据包无法及时写到sk_buffer,就会产生堆积,当NIC内部缓冲区写满后,就会丢弃部分数据,引起丢包。这部分丢包为rx_fifo_errors,在 /proc/net/dev中体现为fifo字段增长,在ifconfig中体现为overruns指标增长。

通知系统内核处理(驱动与Linux内核交互)

这个时候,数据包已经被转移到了sk_buffer中。前文提到,这是驱动程序在内存中分配的一片缓冲区,并且是通过DMA写入的,这种方式不依赖CPU直接将数据写到了内存中,意味着对内核来说,其实并不知道已经有新数据到了内存中。那么如何让内核知道有新数据进来了呢?答案就是中断,通过中断告诉内核有新数据进来了,并需要进行后续处理。

提到中断,就涉及到硬中断和软中断,首先需要简单了解一下它们的区别:

  • 硬中断: 由硬件自己生成,具有随机性,硬中断被CPU接收后,触发执行中断处理程序。中断处理程序只会处理关键性的、短时间内可以处理完的工作,剩余耗时较长工作,会放到中断之后,由软中断来完成。硬中断也被称为上半部分。
  • 软中断: 由硬中断对应的中断处理程序生成,往往是预先在代码里实现好的,不具有随机性。(除此之外,也有应用程序触发的软中断,与本文讨论的网卡收包无关。)也被称为下半部分。

当NIC把数据包通过DMA复制到内核缓冲区sk_buffer后,NIC立即发起一个硬件中断。CPU接收后,首先进入上半部分,网卡中断对应的中断处理程序是网卡驱动程序的一部分,之后由它发起软中断,进入下半部分,开始消费sk_buffer中的数据,交给内核协议栈处理。

通过中断,能够快速及时地响应网卡数据请求,但如果数据量大,那么会产生大量中断请求,CPU大部分时间都忙于处理中断,效率很低。为了解决这个问题,现在的内核及驱动都采用一种叫NAPI(new API)的方式进行数据处理,其原理可以简单理解为 中断+轮询,在数据量大时,一次中断后通过轮询接收一定数量包再返回,避免产生多次中断。

整个中断过程的源码部分比较复杂,并且不同驱动的厂商及版本也会存在一定的区别。 以下调用关系基于Linux-3.10.108及内核自带驱动drivers/net/ethernet/intel/ixgbe:

注意到,enqueue_to_backlog函数中,会对CPU的softnet_data 实例中的接收队列(input_pkt_queue)进行判断,如果队列中的数据长度超过netdev_max_backlog ,那么数据包将直接丢弃,这就产生了丢包。netdev_max_backlog是由系统参数net.core.netdev_max_backlog指定的,默认大小是 1000。

 /*
 * enqueue_to_backlog is called to queue an skb to a per CPU backlog
 * queue (may be a remote CPU queue).
 */
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                  unsigned int *qtail)
{
    struct softnet_data *sd;
    unsigned long flags;

    sd = &per_cpu(softnet_data, cpu);

    local_irq_save(flags);

    rps_lock(sd);

    /* 判断接收队列是否满,队列长度为 netdev_max_backlog  */ 
    if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {


        if (skb_queue_len(&sd->input_pkt_queue)) {
enqueue:
            /*  队列如果不会空,将数据包添加到队列尾  */
            __skb_queue_tail(&sd->input_pkt_queue, skb);
            input_queue_tail_incr_save(sd, qtail);
            rps_unlock(sd);
            local_irq_restore(flags);
            return NET_RX_SUCCESS;
        }   

        /* Schedule NAPI for backlog device
         * We can use non atomic operation since we own the queue lock
         */
        /*  队列如果为空,回到 ____napi_schedule加入poll_list轮询部分,并重新发起软中断  */ 
        if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
            if (!rps_ipi_queued(sd))
                ____napi_schedule(sd, &sd->backlog);
        }   
        goto enqueue;
    }

    /* 队列满则直接丢弃,对应计数器 +1 */ 
    sd->dropped++;
    rps_unlock(sd);

    local_irq_restore(flags);

    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    return NET_RX_DROP;
}

内核会为每个CPU Core都实例化一个softnet_data对象,这个对象中的input_pkt_queue用于管理接收的数据包。假如所有的中断都由一个CPU Core来处理的话,那么所有数据包只能经由这个CPU的input_pkt_queue,如果接收的数据包数量非常大,超过中断处理速度,那么input_pkt_queue中的数据包就会堆积,直至超过netdev_max_backlog,引起丢包。这部分丢包可以在cat /proc/net/softnet_stat的输出结果中进行确认:

其中每行代表一个CPU,第一列是中断处理程序接收的帧数,第二列是由于超过 netdev_max_backlog 而丢弃的帧数。 第三列则是在 net_rx_action 函数中处理数据包超过 netdev_budget 指定数量或运行时间超过2个时间片的次数。在检查线上服务器之后,发现第一行CPU。硬中断的中断号及统计数据可以在/proc/interrupts中看到,对于多队列网卡,当系统启动并加载NIC设备驱动程序模块时,每个RXTX队列会被初始化分配一个唯一的中断向量号,它通知中断处理程序该中断来自哪个NIC队列。在默认情况下,所有队列的硬中断都由CPU 0处理,因此对应的软中断逻辑也会在CPU 0上处理,在服务器 TOP 的输出中,也可以观察到 %si 软中断部分,CPU 0的占比比其他core高出一截。

到这里其实有存在一个疑惑,我们线上服务器的内核版本及网卡都支持NAPI,而NAPI的处理逻辑是不会走到enqueue_to_backlog 中的,enqueue_to_backlog主要是非NAPI的处理流程中使用的。对此,我们觉得可能和当前使用的Docker架构有关,事实上,我们通过net.if.dropped 指标获取到的丢包,都发生在Docker虚拟网卡上,而非宿主机物理网卡上,因此很可能是Docker虚拟网桥转发数据包之后,虚拟网卡层面产生的丢包,这里由于涉及虚拟化部分,就不进一步分析了。

驱动及内核处理过程中的几个重要函数:
(1)注册中断号及中断处理程序,根据网卡是否支持MSI/MSIX,结果为:MSIX → ixgbe_msix_clean_rings,MSI → ixgbe_intr,都不支持 → ixgbe_intr。

/**
 * 文件:ixgbe_main.c
 * ixgbe_request_irq - initialize interrupts
 * @adapter: board private structure
 *
 * Attempts to configure interrupts using the best available
 * capabilities of the hardware and kernel.
 **/
static int ixgbe_request_irq(struct ixgbe_adapter *adapter)
{
    struct net_device *netdev = adapter->netdev;
    int err;

    /* 支持MSIX,调用 ixgbe_request_msix_irqs 设置中断处理程序*/
    if (adapter->flags & IXGBE_FLAG_MSIX_ENABLED)
        err = ixgbe_request_msix_irqs(adapter);
    /* 支持MSI,直接设置 ixgbe_intr 为中断处理程序 */
    else if (adapter->flags & IXGBE_FLAG_MSI_ENABLED)
        err = request_irq(adapter->pdev->irq, &ixgbe_intr, 0,
                  netdev->name, adapter);
    /* 都不支持的情况,直接设置 ixgbe_intr 为中断处理程序 */
    else 
        err = request_irq(adapter->pdev->irq, &ixgbe_intr, IRQF_SHARED,
                  netdev->name, adapter);

    if (err)
        e_err(probe, "request_irq failed, Error %d\n", err);

    return err;
}

/**
 * 文件:ixgbe_main.c
 * ixgbe_request_msix_irqs - Initialize MSI-X interrupts
 * @adapter: board private structure
 *
 * ixgbe_request_msix_irqs allocates MSI-X vectors and requests
 * interrupts from the kernel.
 **/
static int (struct ixgbe_adapter *adapter)
{
    …
    for (vector = 0; vector < adapter->num_q_vectors; vector++) {
        struct ixgbe_q_vector *q_vector = adapter->q_vector[vector];
        struct msix_entry *entry = &adapter->msix_entries[vector];

        /* 设置中断处理入口函数为 ixgbe_msix_clean_rings */
        err = request_irq(entry->vector, &ixgbe_msix_clean_rings, 0,
                  q_vector->name, q_vector);
        if (err) {
            e_err(probe, "request_irq failed for MSIX interrupt '%s' ""Error: %d\n", q_vector->name, err);
            goto free_queue_irqs;
        }…
    }
}

(2)线上的多队列网卡均支持MSIX,中断处理程序入口为ixgbe_msix_clean_rings,里面调用了函数napi_schedule(&q_vector->napi)。

/**
 * 文件:ixgbe_main.c
 **/
static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data)
{
    struct ixgbe_q_vector *q_vector = data;

    /* EIAM disabled interrupts (on this vector) for us */

    if (q_vector->rx.ring || q_vector->tx.ring)
        napi_schedule(&q_vector->napi);

    return IRQ_HANDLED;
}

(3)之后经过一些列调用,直到发起名为NET_RX_SOFTIRQ的软中断。到这里完成了硬中断部分,进入软中断部分,同时也上升到了内核层面。

/**
 * 文件:include/linux/netdevice.h
 *  napi_schedule - schedule NAPI poll
 *  @n: NAPI context
 *
 * Schedule NAPI poll routine to be called if it is not already
 * running.
 */
static inline void napi_schedule(struct napi_struct *n)
{
    if (napi_schedule_prep(n))
    /*  注意下面调用的这个函数名字前是两个下划线 */
        __napi_schedule(n);
}


/**
 * 文件:net/core/dev.c
 * __napi_schedule - schedule for receive
 * @n: entry to schedule
 *
 * The entry's receive function will be scheduled to run.
 * Consider using __napi_schedule_irqoff() if hard irqs are masked.
 */
void __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;

    /*  local_irq_save用来保存中断状态,并禁止中断 */
    local_irq_save(flags);
    /*  注意下面调用的这个函数名字前是四个下划线,传入的 softnet_data 是当前CPU */
    ____napi_schedule(this_cpu_ptr(&softnet_data), n);
    local_irq_restore(flags);
}


/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
                     struct napi_struct *napi)
{
    /* 将 napi_struct 加入 softnet_data 的 poll_list */
    list_add_tail(&napi->poll_list, &sd->poll_list);

    /* 发起软中断 NET_RX_SOFTIRQ */
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

(4)NET_RX_SOFTIRQ对应的软中断处理程序接口是net_rx_action()。

/*
 *  文件:net/core/dev.c
 *  Initialize the DEV module. At boot time this walks the device list and
 *  unhooks any devices that fail to initialise (normally hardware not
 *  present) and leaves us with a valid list of present and active devices.
 *
 */

/*
 *       This is called single threaded during boot, so no need
 *       to take the rtnl semaphore.
 */
static int __init net_dev_init(void)
{
    …
    /*  分别注册TX和RX软中断的处理程序 */
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    …
}

(5)net_rx_action功能就是轮询调用poll方法,这里就是ixgbe_poll。一次轮询的数据包数量不能超过内核参数net.core.netdev_budget指定的数量(默认值300),并且轮询时间不能超过2个时间片。这个机制保证了单次软中断处理不会耗时太久影响被中断的程序。

/* 文件:net/core/dev.c  */
static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = &__get_cpu_var(softnet_data);
    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget;
    void *have;

    local_irq_disable();

    while (!list_empty(&sd->poll_list)) {
        struct napi_struct *n;
        int work, weight;

        /* If softirq window is exhuasted then punt.
         * Allow this to run for 2 jiffies since which will allow
         * an average latency of 1.5/HZ.
         */

        /* 判断处理包数是否超过 netdev_budget 及时间是否超过2个时间片 */
        if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
            goto softnet_break;

        local_irq_enable();

        /* Even though interrupts have been re-enabled, this
         * access is safe because interrupts can only add new
         * entries to the tail of this list, and only ->poll()
         * calls can remove this head entry from the list.
         */
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

        have = netpoll_poll_lock(n);

        weight = n->weight;

        /* This NAPI_STATE_SCHED test is for avoiding a race
         * with netpoll's poll_napi().  Only the entity which
         * obtains the lock and sees NAPI_STATE_SCHED set will
         * actually make the ->poll() call.  Therefore we avoid
         * accidentally calling ->poll() when NAPI is not scheduled.
         */
        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            work = n->poll(n, weight);
            trace_napi_poll(n);
        }

        ……
    } 

}

(6)ixgbe_poll之后的一系列调用就不一一详述了,有兴趣的同学可以自行研究,软中断部分有几个地方会有类似if (static_key_false(&rps_needed))这样的判断,会进入前文所述有丢包风险的enqueue_to_backlog函数。 这里的逻辑为判断是否启用了RPS机制,RPS是早期单队列网卡上将软中断负载均衡到多个CPU Core的技术,它对数据流进行hash并分配到对应的CPU Core上,发挥多核的性能。不过现在基本都是多队列网卡,不会开启这个机制,因此走不到这里,static_key_false是针对默认为false的static key 的优化判断方式。这段调用的最后,deliver_skb会将接收的数据传入一个IP层的数据结构中,至此完成二层的全部处理。

/**
 *  netif_receive_skb - process receive buffer from network
 *  @skb: buffer to process
 *
 *  netif_receive_skb() is the main receive data processing function.
 *  It always succeeds. The buffer may be dropped during processing
 *  for congestion control or by the protocol layers.
 *
 *  This function may only be called from softirq context and interrupts
 *  should be enabled.
 *
 *  Return values (usually ignored):
 *  NET_RX_SUCCESS: no congestion
 *  NET_RX_DROP: packet was dropped
 */
int netif_receive_skb(struct sk_buff *skb)
{
    int ret;

    net_timestamp_check(netdev_tstamp_prequeue, skb);

    if (skb_defer_rx_timestamp(skb))
        return NET_RX_SUCCESS;

    rcu_read_lock();

#ifdef CONFIG_RPS
    /* 判断是否启用RPS机制 */
    if (static_key_false(&rps_needed)) {
        struct rps_dev_flow voidflow, *rflow = &voidflow;
        /* 获取对应的CPU Core */
        int cpu = get_rps_cpu(skb->dev, skb, &rflow);

        if (cpu >= 0) {
            ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
            rcu_read_unlock();
            return ret;
        }
    }
#endif
    ret = __netif_receive_skb(skb);
    rcu_read_unlock();
    return ret;
}

TCP/IP协议栈逐层处理,最终交给用户空间读取

数据包进到IP层之后,经过IP层、TCP层处理(校验、解析上层协议,发送给上层协议),放入socket buffer,在应用程序执行read() 系统调用时,就能从socket buffer中将新数据从内核区拷贝到用户区,完成读取。

这里的socket buffer大小即TCP接收窗口,TCP由于具备流量控制功能,能动态调整接收窗口大小,因此数据传输阶段不会出现由于socket buffer接收队列空间不足而丢包的情况(但UDP及TCP握手阶段仍会有)。涉及TCP/IP协议的部分不是此次丢包问题的研究重点,因此这里不再赘述。

网卡队列

查看网卡型号

  # lspci -vvv | grep Eth
01:00.0 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 03)
        Subsystem: Dell Ethernet 10G 4P X540/I350 rNDC
01:00.1 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 03)
        Subsystem: Dell Ethernet 10G 4P X540/I350 rNDC


# lspci -vvv
07:00.0 Ethernet controller: Intel Corporation I350 Gigabit Network Connection (rev 01)
        Subsystem: Dell Gigabit 4P X540/I350 rNDC
        Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
        Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
        Latency: 0, Cache Line Size: 128 bytes
        Interrupt: pin D routed to IRQ 19
        Region 0: Memory at 92380000 (32-bit, non-prefetchable) [size=512K]
        Region 3: Memory at 92404000 (32-bit, non-prefetchable) [size=16K]
        Expansion ROM at 92a00000 [disabled] [size=512K]
        Capabilities: [40] Power Management version 3
                Flags: PMEClk- DSI+ D1- D2- AuxCurrent=0mA PME(D0+,D1-,D2-,D3hot+,D3cold+)
                Status: D0 NoSoftRst+ PME-Enable- DSel=0 DScale=1 PME-
        Capabilities: [50] MSI: Enable- Count=1/1 Maskable+ 64bit+
                Address: 0000000000000000  Data: 0000
                Masking: 00000000  Pending: 00000000
        Capabilities: [70] MSI-X: Enable+ Count=10 Masked-
                Vector table: BAR=3 offset=00000000
                PBA: BAR=3 offset=00002000

可以看出,网卡的中断机制是MSI-X,即网卡的每个队列都可以分配中断(MSI-X支持2048个中断)。

网卡队列

 ...
 #define IXGBE_MAX_MSIX_VECTORS_82599    0x40
...




  u16 ixgbe_get_pcie_msix_count_generic(struct ixgbe_hw *hw)
 {
     u16 msix_count;
     u16 max_msix_count;
     u16 pcie_offset;

     switch (hw->mac.type) {
     case ixgbe_mac_82598EB:
         pcie_offset = IXGBE_PCIE_MSIX_82598_CAPS;
         max_msix_count = IXGBE_MAX_MSIX_VECTORS_82598;
         break;
     case ixgbe_mac_82599EB:
     case ixgbe_mac_X540:
     case ixgbe_mac_X550:
     case ixgbe_mac_X550EM_x:
     case ixgbe_mac_x550em_a:
         pcie_offset = IXGBE_PCIE_MSIX_82599_CAPS;
         max_msix_count = IXGBE_MAX_MSIX_VECTORS_82599;
         break;
     default:
         return 1;
     }
 ...

根据网卡型号确定驱动中定义的网卡队列,可以看到X540网卡驱动中定义最大支持的IRQ Vector为0x40(数值:64)。

 static int ixgbe_acquire_msix_vectors(struct ixgbe_adapter *adapter)
 {
     struct ixgbe_hw *hw = &adapter->hw;
     int i, vectors, vector_threshold;

     /* We start by asking for one vector per queue pair with XDP queues
      * being stacked with TX queues.
      */
     vectors = max(adapter->num_rx_queues, adapter->num_tx_queues);
     vectors = max(vectors, adapter->num_xdp_queues);

     /* It is easy to be greedy for MSI-X vectors. However, it really
      * doesn't do much good if we have a lot more vectors than CPUs. We'll
      * be somewhat conservative and only ask for (roughly) the same number
      * of vectors as there are CPUs.
      */
     vectors = min_t(int, vectors, num_online_cpus());

通过加载网卡驱动,获取网卡型号和网卡硬件的队列数;但是在初始化misx vector的时候,还会结合系统在线CPU的数量,通过Sum = Min(网卡队列,CPU Core) 来激活相应的网卡队列数量,并申请Sum个中断号。

如果CPU数量小于64,会生成CPU数量的队列,也就是每个CPU会产生一个external IRQ。

我们线上的CPU一般是48个逻辑core,就会生成48个中断号,由于我们是两块网卡做了bond,也就会生成96个中断号。

验证与复现网络丢包

通过霸爷的 一篇文章,我们在测试环境做了测试,发现测试环境的中断确实有集中在CPU 0的情况,下面使用systemtap诊断测试环境软中断分布的方法:

global hard, soft, wq

probe irq_handler.entry {
hard[irq, dev_name]++;
}

probe timer.s(1) {
println("==irq number:dev_name")
foreach( [irq, dev_name] in hard- limit 5) {
printf("%d,%s->%d\n", irq, kernel_string(dev_name), hard[irq, dev_name]);      
}

println("==softirq cpu:h:vec:action")
foreach( [c,h,vec,action] in soft- limit 5) {
printf("%d:%x:%x:%s->%d\n", c, h, vec, symdata(action), soft[c,h,vec,action]);      
}


println("==workqueue wq_thread:work_func")
foreach( [wq_thread,work_func] in wq- limit 5) {
printf("%x:%x->%d\n", wq_thread, work_func, wq[wq_thread, work_func]); 
}

println("\n")
delete hard
delete soft
delete wq
}

probe softirq.entry {
soft[cpu(), h,vec,action]++;
}

probe workqueue.execute {
wq[wq_thread, work_func]++
}


probe begin {
println("~")
}

下面执行i.stap 的结果:

==irq number:dev_name
87,eth0-0->1693
90,eth0-3->1263
95,eth1-3->746
92,eth1-0->703
89,eth0-2->654
==softirq cpu:h:vec:action
0:ffffffff81a83098:ffffffff81a83080:0xffffffff81461a00->8928
0:ffffffff81a83088:ffffffff81a83080:0xffffffff81084940->626
0:ffffffff81a830c8:ffffffff81a83080:0xffffffff810ecd70->614
16:ffffffff81a83088:ffffffff81a83080:0xffffffff81084940->225
16:ffffffff81a830c8:ffffffff81a83080:0xffffffff810ecd70->224
==workqueue wq_thread:work_func
ffff88083062aae0:ffffffffa01c53d0->10
ffff88083062aae0:ffffffffa01ca8f0->10
ffff88083420a080:ffffffff81142160->2
ffff8808343fe040:ffffffff8127c9d0->2
ffff880834282ae0:ffffffff8133bd20->1

下面是action对应的符号信息:

addr2line -e /usr/lib/debug/lib/modules/2.6.32-431.20.3.el6.mt20161028.x86_64/vmlinux ffffffff81461a00
/usr/src/debug/kernel-2.6.32-431.20.3.el6/linux-2.6.32-431.20.3.el6.mt20161028.x86_64/net/core/dev.c:4013

打开这个文件,我们发现它是在执行 static void net_rx_action(struct softirq_action *h)这个函数,而这个函数正是前文提到的,NET_RX_SOFTIRQ 对应的软中断处理程序。因此可以确认网卡的软中断在机器上分布非常不均,而且主要集中在CPU 0上。通过/proc/interrupts能确认硬中断集中在CPU 0上,因此软中断也都由CPU 0处理,如何优化网卡的中断成为了我们关注的重点。

优化策略

CPU亲缘性

前文提到,丢包是因为队列中的数据包超过了 netdev_max_backlog 造成了丢弃,因此首先想到是临时调大netdev_max_backlog能否解决燃眉之急,事实证明,对于轻微丢包调大参数可以缓解丢包,但对于大量丢包则几乎不怎么管用,内核处理速度跟不上收包速度的问题还是客观存在,本质还是因为单核处理中断有瓶颈,即使不丢包,服务响应速度也会变慢。因此如果能同时使用多个CPU Core来处理中断,就能显著提高中断处理的效率,并且每个CPU都会实例化一个softnet_data对象,队列数也增加了。

中断亲缘性设置

通过设置中断亲缘性,可以让指定的中断向量号更倾向于发送给指定的CPU Core来处理,俗称“绑核”。命令grep eth /proc/interrupts的第一列可以获取网卡的中断号,如果是多队列网卡,那么就会有多行输出:

中断的亲缘性设置可以在 cat /proc/irq/${中断号}/smp_affinity 或 cat /proc/irq/${中断号}/smp_affinity_list 中确认,前者是16进制掩码形式,后者是以CPU Core序号形式。例如下图中,将16进制的400转换成2进制后,为 10000000000,“1”在第10位上,表示亲缘性是第10个CPU Core。

那为什么中断号只设置一个CPU Core呢?而不是为每一个中断号设置多个CPU Core平行处理。我们经过测试,发现当给中断设置了多个CPU Core后,它也仅能由设置的第一个CPU Core来处理,其他的CPU Core并不会参与中断处理,原因猜想是当CPU可以平行收包时,不同的核收取了同一个queue的数据包,但处理速度不一致,导致提交到IP层后的顺序也不一致,这就会产生乱序的问题,由同一个核来处理可以避免了乱序问题。

但是,当我们配置了多个Core处理中断后,发现Redis的慢查询数量有明显上升,甚至部分业务也受到了影响,慢查询增多直接导致可用性降低,因此方案仍需进一步优化。

Redis进程亲缘性设置

如果某个CPU Core正在处理Redis的调用,执行到一半时产生了中断,那么CPU不得不停止当前的工作转而处理中断请求,中断期间Redis也无法转交给其他core继续运行,必须等处理完中断后才能继续运行。Redis本身定位就是高速缓存,线上的平均端到端响应时间小于1ms,如果频繁被中断,那么响应时间必然受到极大影响。容易想到,由最初的CPU 0单核处理中断,改进到多核处理中断,Redis进程被中断影响的几率增大了,因此我们需要对Redis进程也设置CPU亲缘性,使其与处理中断的Core互相错开,避免受到影响。

使用命令taskset可以为进程设置CPU亲缘性,操作十分简单,一句taskset -cp cpu-list pid即可完成绑定。经过一番压测,我们发现使用8个core处理中断时,流量直至打满双万兆网卡也不会出现丢包,因此决定将中断的亲缘性设置为物理机上前8个core,Redis进程的亲缘性设置为剩下的所有core。调整后,确实有明显的效果,慢查询数量大幅优化,但对比初始情况,仍然还是高了一些些,还有没有优化空间呢?

通过观察,我们发现一个有趣的现象,当只有CPU 0处理中断时,Redis进程更倾向于运行在CPU 0,以及CPU 0同一物理CPU下的其他核上。于是有了以下推测:我们设置的中断亲缘性,是直接选取了前8个核心,但这8个core却可能是来自两块物理CPU的,在/proc/cpuinfo中,通过字段processor和physical id 能确认这一点,那么响应慢是否和物理CPU有关呢?物理CPU又和NUMA架构关联,每个物理CPU对应一个NUMA node,那么接下来就要从NUMA角度进行分析。

NUMA

SMP 架构

随着单核CPU的频率在制造工艺上的瓶颈,CPU制造商的发展方向也由纵向变为横向:从CPU频率转为每瓦性能。CPU也就从单核频率时代过渡到多核性能协调。

SMP(对称多处理结构):即CPU共享所有资源,例如总线、内存、IO等。

SMP 结构:一个物理CPU可以有多个物理Core,每个Core又可以有多个硬件线程。即:每个HT有一个独立的L1 cache,同一个Core下的HT共享L2 cache,同一个物理CPU下的多个core共享L3 cache。

下图(摘自 内核月谈)中,一个x86 CPU有4个物理Core,每个Core有两个HT(Hyper Thread)。

NUMA 架构

在前面的FSB(前端系统总线)结构中,当CPU不断增长的情况下,共享的系统总线就会因为资源竞争(多核争抢总线资源以访问北桥上的内存)而出现扩展和性能问题。

在这样的背景下,基于SMP架构上的优化,设计出了NUMA(Non-Uniform Memory Access)—— 非均匀内存访问。

内存控制器芯片被集成到处理器内部,多个处理器通过QPI链路相连,DRAM也就有了远近之分。(如下图所示:摘自 CPU Cache)

CPU 多层Cache的性能差异是很巨大的,比如:L1的访问时长1ns,L2的时长3ns...跨node的访问会有几十甚至上百倍的性能损耗。

NUMA 架构下的中断优化

这时我们再回归到中断的问题上,当两个NUMA节点处理中断时,CPU实例化的softnet_data以及驱动分配的sk_buffer都可能是跨node的,数据接收后对上层应用Redis来说,跨node访问的几率也大大提高,并且无法充分利用L2、L3 cache,增加了延时。

同时,由于Linux wake affinity 特性,如果两个进程频繁互动,调度系统会觉得它们很有可能共享同样的数据,把它们放到同一CPU核心或NUMA Node有助于提高缓存和内存的访问性能,所以当一个进程唤醒另一个的时候,被唤醒的进程可能会被放到相同的CPU core或者相同的NUMA节点上。此特性对中断唤醒进程时也起作用,在上一节所述的现象中,所有的网络中断都分配给CPU 0去处理,当中断处理完成时,由于wakeup affinity特性的作用,所唤醒的用户进程也被安排给CPU 0或其所在的numa节点上其他core。而当两个NUMA node处理中断时,这种调度特性有可能导致Redis进程在CPU core之间频繁迁移,造成性能损失。

综合上述,将中断都分配在同一NUMA Node中,中断处理函数和应用程序充分利用同NUMA下的L2、L3缓存、以及同node下的内存,结合调度系统的wake affinity特性,能够更进一步降低延迟。

参考文档

  1. Intel 官方文档
  2. Redhat 官方文档

作者简介

骁雄,14年加入美团点评,主要从事MySQL、Redis数据库运维,高可用和相关运维平台建设。

春林,17年加入美团点评,毕业后一直深耕在运维线,从网络工程师到Oracle DBA再到MySQL DBA 多种岗位转变,现在美大主要职责Redis运维开发和优化工作。

美团点评DBA团队招聘各类DBA人才,Base北京上海均可。我们致力于为公司提供稳定、可靠、高效的在线存储服务,打造业界领先的数据库团队。这里有基于Redis Cluster构建的大规模分布式缓存系统Squirrel,也有基于Tair进行大刀阔斧改进的分布式KV存储系统Cellar,还有数千各类架构的MySQL实例,每天提供万亿级的OLTP访问请求。真正的海量、分布式、高并发环境。欢迎各位朋友推荐或自荐至jinlong.cai#dianping.com。

美团针对Redis Rehash机制的探索和实践

$
0
0

背景

Squirrel(松鼠)是美团技术团队基于Redis Cluster打造的缓存系统。经过不断的迭代研发,目前已形成一整套自动化运维体系:涵盖一键运维集群、细粒度的监控、支持自动扩缩容以及热点Key监控等完整的解决方案。同时服务端通过Docker进行部署,最大程度的提高运维的灵活性。分布式缓存Squirrel产品自2015年上线至今,已在美团内部广泛使用,存储容量超过60T,日均调用量也超过万亿次,逐步成为美团目前最主要的缓存系统之一。

随着使用的量和场景不断深入,Squirrel团队也不断发现Redis的若干"坑"和不足,因此也在持续的改进Redis以支撑美团内部快速发展的业务需求。本文尝试分享在运维过程中踩过的Redis Rehash机制的一些坑以及我们的解决方案,其中在高负载情况下物理机发生丢包的现象和解决方案已经写成博客。感兴趣的同学可以参考: Redis 高负载下的中断优化

案例

Redis 满容状态下由于Rehash导致大量Key驱逐

我们先来看一张监控图(上图,我们线上真实案例),Redis在满容有驱逐策略的情况下,Master/Slave 均有大量的Key驱逐淘汰,导致Master/Slave 主从不一致。

Root Cause 定位

由于Slave内存区域比Master少一个repl-backlog buffer(线上一般配置为128M),正常情况下Master到达满容后根据驱逐策略淘汰Key并同步给Slave。所以Slave这种情况下不会因满容触发驱逐。

按照以往经验,排查思路主要聚焦在造成Slave内存陡增的问题上,包括客户端连接、输入/输出缓冲区、业务数据存取访问、网路抖动等导致Redis内存陡增的所有外部因素,通过Redis监控和业务链路监控均没有定位成功。

于是,通过梳理Redis源码,我们尝试将目光投向了Redis会占用内存开销的一个重要机制——Redis Rehash。

Redis Rehash 内部实现

在Redis中,键值对(Key-Value Pair)存储方式是由字典(Dict)保存的,而字典底层是通过哈希表来实现的。通过哈希表中的节点保存字典中的键值对。类似Java中的HashMap,将Key通过哈希函数映射到哈希表节点位置。

接下来我们一步步来分析Redis Dict Reash的机制和过程。

(1) Redis 哈希表结构体:

/* hash表结构定义 */
typedef struct dictht { 
    dictEntry **table;   // 哈希表数组
    unsigned long size;  // 哈希表的大小
    unsigned long sizemask; // 哈希表大小掩码
    unsigned long used;  // 哈希表现有节点的数量
} dictht;

实体化一下,如下图所指一个大小为4的空哈希表(Redis默认初始化值为4):

(2) Redis 哈希桶
Redis 哈希表中的table数组存放着哈希桶结构(dictEntry),里面就是Redis的键值对;类似Java实现的HashMap,Redis的dictEntry也是通过链表(next指针)方式来解决hash冲突:

/* 哈希桶 */
typedef struct dictEntry { 
    void *key;     // 键定义
    // 值定义
    union { 
        void *val;    // 自定义类型
        uint64_t u64; // 无符号整形
        int64_t s64;  // 有符号整形
        double d;     // 浮点型
    } v;     
    struct dictEntry *next;  //指向下一个哈希表节点
} dictEntry;

(3) 字典
Redis Dict 中定义了两张哈希表,是为了后续字典的扩展作Rehash之用:

/* 字典结构定义 */
typedef struct dict { 
    dictType *type;  // 字典类型
    void *privdata;  // 私有数据
    dictht ht[2];    // 哈希表[两个]
    long rehashidx;   // 记录rehash 进度的标志,值为-1表示rehash未进行
    int iterators;   //  当前正在迭代的迭代器数
} dict;

总结一下:

  • 在Cluster模式下,一个Redis实例对应一个RedisDB(db0);
  • 一个RedisDB对应一个Dict;
  • 一个Dict对应2个Dictht,正常情况只用到ht[0];ht[1] 在Rehash时使用。

如上,我们回顾了一下Redis KV存储的实现。(Redis内部还有其他结构体,由于跟Rehash不涉及,不再赘述)

我们知道当HashMap中由于Hash冲突(负载因子)超过某个阈值时,出于链表性能的考虑,会进行Resize的操作。Redis也一样【Redis中通过dictExpand()实现】。我们看一下Redis中的实现方式:

/* 根据相关触发条件扩展字典 */
static int _dictExpandIfNeeded(dict *d) 
{ 
    if (dictIsRehashing(d)) return DICT_OK;  // 如果正在进行Rehash,则直接返回
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);  // 如果ht[0]字典为空,则创建并初始化ht[0]  
    /* (ht[0].used/ht[0].size)>=1前提下,
       当满足dict_can_resize=1或ht[0].used/t[0].size>5时,便对字典进行扩展 */
    if (d->ht[0].used >= d->ht[0].size && 
        (dict_can_resize || 
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) 
    { 
        return dictExpand(d, d->ht[0].used*2);   // 扩展字典为原来的2倍
    } 
    return DICT_OK; 
}


...

/* 计算存储Key的bucket的位置 */
static int _dictKeyIndex(dict *d, const void *key) 
{ 
    unsigned int h, idx, table; 
    dictEntry *he; 

    /* 检查是否需要扩展哈希表,不足则扩展 */ 
    if (_dictExpandIfNeeded(d) == DICT_ERR)  
        return -1; 
    /* 计算Key的哈希值 */ 
    h = dictHashKey(d, key); 
    for (table = 0; table <= 1; table++) { 
        idx = h & d->ht[table].sizemask;  //计算Key的bucket位置
        /* 检查节点上是否存在新增的Key */ 
        he = d->ht[table].table[idx]; 
        /* 在节点链表检查 */ 
        while(he) { 
            if (key==he->key || dictCompareKeys(d, key, he->key)) 
                return -1; 
            he = he->next;
        } 
        if (!dictIsRehashing(d)) break;  // 扫完ht[0]后,如果哈希表不在rehashing,则无需再扫ht[1]
    } 
    return idx; 
} 

...

/* 将Key插入哈希表 */
dictEntry *dictAddRaw(dict *d, void *key) 
{ 
    int index; 
    dictEntry *entry; 
    dictht *ht; 

    if (dictIsRehashing(d)) _dictRehashStep(d);  // 如果哈希表在rehashing,则执行单步rehash

    /* 调用_dictKeyIndex() 检查键是否存在,如果存在则返回NULL */ 
    if ((index = _dictKeyIndex(d, key)) == -1) 
        return NULL; 


    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; 
    entry = zmalloc(sizeof(*entry));   // 为新增的节点分配内存
    entry->next = ht->table[index];  //  将节点插入链表表头
    ht->table[index] = entry;   // 更新节点和桶信息
    ht->used++;    //  更新ht

    /* 设置新节点的键 */ 
    dictSetKey(d, entry, key); 
    return entry; 
}

...
/* 添加新键值对 */
int dictAdd(dict *d, void *key, void *val) 
{ 
    dictEntry *entry = dictAddRaw(d,key);  // 添加新键

    if (!entry) return DICT_ERR;  // 如果键存在,则返回失败
    dictSetVal(d, entry, val);   // 键不存在,则设置节点值
    return DICT_OK; 
}

继续dictExpand的源码实现:

int dictExpand(dict *d, unsigned long size) 
{ 
    dictht n; // 新哈希表
    unsigned long realsize = _dictNextPower(size);  // 计算扩展或缩放新哈希表的大小(调用下面函数_dictNextPower())

    /* 如果正在rehash或者新哈希表的大小小于现已使用,则返回error */ 
    if (dictIsRehashing(d) || d->ht[0].used > size) 
        return DICT_ERR; 

    /* 如果计算出哈希表size与现哈希表大小一样,也返回error */ 
    if (realsize == d->ht[0].size) return DICT_ERR; 

    /* 初始化新哈希表 */ 
    n.size = realsize; 
    n.sizemask = realsize-1; 
    n.table = zcalloc(realsize*sizeof(dictEntry*));  // 为table指向dictEntry 分配内存
    n.used = 0; 

    /* 如果ht[0] 为空,则初始化ht[0]为当前键值对的哈希表 */ 
    if (d->ht[0].table == NULL) { 
        d->ht[0] = n; 
        return DICT_OK; 
    } 

    /* 如果ht[0]不为空,则初始化ht[1]为当前键值对的哈希表,并开启渐进式rehash模式 */ 
    d->ht[1] = n; 
    d->rehashidx = 0; 
    return DICT_OK; 
}
...
static unsigned long _dictNextPower(unsigned long size) { 
    unsigned long i = DICT_HT_INITIAL_SIZE;  // 哈希表的初始值:4


    if (size >= LONG_MAX) return LONG_MAX; 
    /* 计算新哈希表的大小:第一个大于等于size的2的N 次方的数值 */
    while(1) { 
        if (i >= size) 
            return i; 
        i *= 2; 
    } 
}

总结一下具体逻辑实现:

可以确认当Redis Hash冲突到达某个条件时就会触发dictExpand()函数来扩展HashTable。

DICT_HT_INITIAL_SIZE初始化值为4,通过上述表达式,取当4*2^n >= ht[0].used*2的值作为字典扩展的size大小。即为:ht[1].size 的值等于第一个大于等于ht[0].used*2的2^n的数值。

Redis通过dictCreate()创建词典,在初始化中,table指针为Null,所以两个哈希表ht[0].table和ht[1].table都未真正分配内存空间。只有在dictExpand()字典扩展时才给table分配指向dictEntry的内存。

由上可知,当Redis触发Resize后,就会动态分配一块内存,最终由ht[1].table指向,动态分配的内存大小为:realsize*sizeof(dictEntry*),table指向dictEntry*的一个指针,大小为8bytes(64位OS),即ht[1].table需分配的内存大小为:8*2*2^n (n大于等于2)。

梳理一下哈希表大小和内存申请大小的对应关系:

ht[0].size触发Resize时,ht[1]需分配的内存
464bytes
8128bytes
16256bytes
......
655361024K
......
8388608128M
16777216256M
33554432512M
671088641024M
......

复现验证

我们通过测试环境数据来验证一下,当Redis Rehash过程中,内存真正的占用情况。

上述两幅图中,Redis Key个数突破Redis Resize的临界点,当Key总数稳定且Rehash完成后,Redis内存(Slave)从3586M降至为3522M:3586-3522=64M。即验证上述Redis在Resize至完成的中间状态,会维持一段时间内存消耗,且占用内存的值为上文列表相应的内存空间。

进一步观察一下Redis内部统计信息:

/* Redis节点800万左右Key时候的Dict状态信息:只有ht[0]信息。*/
"[Dictionary HT]
Hash table 0 stats (main hash table):
 table size: 8388608
 number of elements: 8003582
 different slots: 5156314
 max chain length: 9
 avg chain length (counted): 1.55
 avg chain length (computed): 1.55
 Chain length distribution:
   0: 3232294 (38.53%)
   1: 3080243 (36.72%)
   2: 1471920 (17.55%)
   3: 466676 (5.56%)
   4: 112320 (1.34%)
   5: 21301 (0.25%)
   6: 3361 (0.04%)
   7: 427 (0.01%)
   8: 63 (0.00%)
   9: 3 (0.00%)"

/* Redis节点840万左右Key时候的Dict状态信息正在Rehasing中,包含了ht[0]和ht[1]信息。*/
"[Dictionary HT]
[Dictionary HT]
Hash table 0 stats (main hash table):
 table size: 8388608
 number of elements: 8019739
 different slots: 5067892
 max chain length: 9
 avg chain length (counted): 1.58
 avg chain length (computed): 1.58
 Chain length distribution:
   0: 3320716 (39.59%)
   1: 2948053 (35.14%)
   2: 1475756 (17.59%)
   3: 491069 (5.85%)
   4: 123594 (1.47%)
   5: 24650 (0.29%)
   6: 4135 (0.05%)
   7: 553 (0.01%)
   8: 78 (0.00%)
   9: 4 (0.00%)
Hash table 1 stats (rehashing target):
 table size: 16777216
 number of elements: 384321
 different slots: 305472
 max chain length: 6
 avg chain length (counted): 1.26
 avg chain length (computed): 1.26
 Chain length distribution:
   0: 16471744 (98.18%)
   1: 238752 (1.42%)
   2: 56041 (0.33%)
   3: 9378 (0.06%)
   4: 1167 (0.01%)
   5: 119 (0.00%)
   6: 15 (0.00%)"

/* Redis节点840万左右Key时候的Dict状态信息(Rehash完成后);ht[0].size从8388608扩展到了16777216。*/
"[Dictionary HT]
Hash table 0 stats (main hash table):
 table size: 16777216
 number of elements: 8404060
 different slots: 6609691
 max chain length: 7
 avg chain length (counted): 1.27
 avg chain length (computed): 1.27
 Chain length distribution:
   0: 10167525 (60.60%)
   1: 5091002 (30.34%)
   2: 1275938 (7.61%)
   3: 213024 (1.27%)
   4: 26812 (0.16%)
   5: 2653 (0.02%)
   6: 237 (0.00%)
   7: 25 (0.00%)"

经过Redis Rehash内部机制的深入、Redis状态监控和Redis内部统计信息,我们可以得出结论:
当Redis 节点中的Key总量到达临界点后,Redis就会触发Dict的扩展,进行Rehash。申请扩展后相应的内存空间大小。

如上,Redis在满容驱逐状态下,Redis Rehash是导致Redis Master和Slave大量触发驱逐淘汰的根本原因。

除了导致满容驱逐淘汰,Redis Rehash还会引起其他一些问题:

  • 在tablesize级别与现有Keys数量不在同一个区间内,主从切换后,由于Redis全量同步,从库tablesize降为与现有Key匹配值,导致内存倾斜;
  • Redis Cluster下的某个分片由于Key数量相对较多提前Resize,导致集群分片内存不均。
    等等...

Redis Rehash机制优化

那么针对在Redis满容驱逐状态下,如何避免因Rehash而导致Redis抖动的这种问题。

  • 我们在Redis Rehash源码实现的逻辑上,加上了一个判断条件,如果现有的剩余内存不够触发Rehash操作所需申请的内存大小,即不进行Resize操作;
  • 通过提前运营进行规避,比如容量预估时将Rehash占用的内存考虑在内,或者通过监控定时扩容。

Redis Rehash机制除了会影响上述内存管理和使用外,也会影响Redis其他内部与之相关联的功能模块。下面我们分享一下由于Rehash机制而踩到的第二个坑。

Redis使用Scan清理Key由于Rehash导致清理数据不彻底

Squirrel平台提供给业务清理Key的API后台逻辑,是通过Scan来实现的。实际线上运行效果并不是每次都能完全清理干净。即通过Scan扫描清理相匹配的Key,较低频率会有遗漏、Key未被全部清理掉的现象。有了前几次的相关经验后,我们直接从原理入手。

Scan原理

为了高效地匹配出数据库中所有符合给定模式的Key,Redis提供了Scan命令。该命令会在每次调用的时候返回符合规则的部分Key以及一个游标值Cursor(初始值使用0),使用每次返回Cursor不断迭代,直到Cursor的返回值为0代表遍历结束。

Redis官方定义Scan特点如下:

  1. 整个遍历从开始到结束期间, 一直存在于Redis数据集内的且符合匹配模式的所有Key都会被返回;
  2. 如果发生了rehash,同一个元素可能会被返回多次,遍历过程中新增或者删除的Key可能会被返回,也可能不会。

具体实现

上述提及Redis的Keys是以Dict方式来存储的,正常只要一次遍历Dict中所有Hash桶就可以完整扫描出所有Key。但是在实际使用中,Redis Dict是有状态的,会随着Key的增删不断变化。

接下来根据Dict四种状态来分析一下Scan的不同实现。

Dict的四种状态场景:

  1. 字典tablesize保持不变,没有扩缩容;
  2. 字典Resize,Dict扩大了(完成状态);
  3. 字典Resize,Dict缩小了(完成状态);
  4. 字典正在Rehashing(扩展或收缩)。

(1) 字典tablesize保持不变,在Redis Dict稳定的状态下,直接顺序遍历即可;
(2) 字典Resize,Dict扩大了,如果还是按照顺序遍历,就会导致扫描大量重复Key。比如字典tablesize从8变成了16,假设之前访问的是3号桶,那么表扩展后则是继续访问4~15号桶;但是,原先的0~3号桶中的数据在Dict长度变大后被迁移到8~11号桶中,因此,遍历8~11号桶的时候会有大量的重复Key被返回;
(3) 字典Resize,Dict缩小了,如果还是按照顺序遍历,就会导致大量的Key被遗漏。比如字典tablesize从8变成了4,假设当前访问的是3号桶,那么下一次则会直接返回遍历结束了;但是之前4~7号桶中的数据在缩容后迁移带可0~3号桶中,因此这部分Key就无法扫描到;
(4) 字典正在Rehashing,这种情况如(2)和(3)情况一下,要么大量重复扫描、要么遗漏很多Key。

那么在Dict非稳定状态,即发生Rehash的情况下,Scan要如何保证原有的Key都能遍历出来,又尽少可能重复扫描呢?Redis Scan通过Hash桶掩码的高位顺序访问来解决。

高位顺序访问即按照Dict sizemask(掩码),在有效位(上图中Dict sizemask为3)上从高位开始加一枚举;低位则按照有效位的低位逐步加一访问。
低位序:0→1→2→3→4→5→6→7
高位序:0→4→2→6→1→5→3→7

Scan采用高位序访问的原因,就是为了实现Redis Dict在Rehash时尽可能少重复扫描返回Key。

举个例子,如果Dict的tablesize从8扩展到了16,梳理一下Scan扫描方式:

  1. Dict(8) 从Cursor 0开始扫描;
  2. 准备扫描Cursor 6时发生Resize,扩展为之前的2倍,并完成Rehash;
  3. 客户端这时开始从Dict(16)的Cursor 6继续迭代;
  4. 这时按照 6→14→1→9→5→13→3→11→7→15 Scan完成。

可以看出,高位序Scan在Dict Rehash时即可以避免重复遍历,又能完整返回原始的所有Key。同理,字典缩容时也一样,字典缩容可以看出是反向扩容。

上述是Scan的理论基础,我们看一下Redis源码如何实现。

(1) 非Rehashing 状态下的实现:

 if (!dictIsRehashing(d)) {     // 判断是否正在rehashing,如果不在则只有ht[0]
        t0 = &(d->ht[0]);  // ht[0]
        m0 = t0->sizemask;  // 掩码

        /* Emit entries at cursor */
        de = t0->table[v & m0];  // 目标桶
        while (de) {           
            fn(privdata, de);
            de = de->next;       // 遍历桶中所有节点,并通过回调函数fn()返回
        }
     ...
      /* 反向二进制迭代算法具体实现逻辑——游标实现的精髓 */
     /* Set unmasked bits so incrementing the reversed cursor
     * operates on the masked bits of the smaller table */
    v |= ~m0;

    /* Increment the reverse cursor */
    v = rev(v);
    v++;
    v = rev(v);

    return v;
}

源码中Redis将Cursor的计算通过Reverse Binary Iteration(反向二进制迭代算法)来实现上述的高位序扫描方式。

(2) Rehashing 状态下的实现:

...
  else {    // 否则说明正在rehashing,就存在两个哈希表ht[0]、ht[1]
        t0 = &d->ht[0];
        t1 = &d->ht[1];  // 指向两个哈希表

        /* Make sure t0 is the smaller and t1 is the bigger table */
        if (t0->size > t1->size) {  确保t0小于t1
            t0 = &d->ht[1];
            t1 = &d->ht[0];  
        }

        m0 = t0->sizemask;
        m1 = t1->sizemask;  // 相对应的掩码

        /* Emit entries at cursor */
        /* 迭代(小表)t0桶中的所有节点 */
        de = t0->table[v & m0];
        while (de) {   
            fn(privdata, de);
            de = de->next;
        }

        /* Iterate over indices in larger table that are the expansion
         * of the index pointed to by the cursor in the smaller table */
        /* */

        do {
            /* Emit entries at cursor */
            /* 迭代(大表)t1 中所有节点,循环迭代,会把小表没有覆盖的slot全部扫描一遍 */ 
            de = t1->table[v & m1];
            while (de) {
                fn(privdata, de);
                de = de->next;
            }

            /* Increment bits not covered by the smaller mask */
            v = (((v | m0) + 1) & ~m0) | (v & m0);

            /* Continue while bits covered by mask difference is non-zero */
        } while (v & (m0 ^ m1));
    }

    /* Set unmasked bits so incrementing the reversed cursor
     * operates on the masked bits of the smaller table */
    v |= ~m0;

    /* Increment the reverse cursor */
    v = rev(v);
    v++;
    v = rev(v);

    return v;

如上Rehashing时,Redis 通过else分支实现该过程中对两张Hash表进行扫描访问。

梳理一下逻辑流程:

Redis在处理dictScan()时,上面细分的四个场景的实现分成了两个逻辑:

  1. 此时不在Rehashing的状态:
    这种状态,即Dict是静止的。针对这种状态下的上述三种场景,Redis采用上述的Reverse Binary Iteration(反向二进制迭代算法):
    Ⅰ. 首先对游标(Cursor)二进制位翻转;
    Ⅱ. 再对翻转后的值加1;
    Ⅲ. 最后再次对Ⅱ的结果进行翻转。

通过穷举高位,依次向低位推进的方式(即高位序访问的实现)来确保所有元素都会被遍历到。

这种算法已经尽可能减少重复元素的返回,但是实际实现和逻辑中还是会有可能存在重复返回,比如在Dict缩容时,高位合并到低位桶中,低位桶中的元素就会被重复取出。

  1. 正在Rehashing的状态:
    Redis在Rehashing状态的时候,dictScan()实现通过一次性扫描现有的两种字典表,避免中间状态无法维护。
    具体实现就是在遍历完小表Cursor位置后,将小表Cursor位置可能Rehash到的大表所有位置全部遍历一遍,然后再返回遍历元素和下一个小表遍历位置。

Root Cause 定位

Rehashing状态时,游标迭代主要逻辑代码实现:

             /* Increment bits not covered by the smaller mask */
            v = (((v | m0) + 1) & ~m0) | (v & m0);   //BUG

Ⅰ. v低位加1向高位进位;
Ⅱ. 去掉v最前面和最后面的部分,只保留v相较于m0的高位部分;
Ⅲ. 保留v的低位,高位不断加1。即低位不变,高位不断加1,实现了小表到大表桶的关联。

举个例子,如果Dict的tablesize从8扩展到了32,梳理一下Scan扫描方式:

  1. Dict(8) 从Cursor 0开始扫描;
  2. 准备扫描Cursor 4时发生Resize,扩展为之前的4倍,Rehashing;
  3. 客户端先访问Dict(8)中的4号桶,然后再到Dict(32)上访问:4→20→12→28;
  4. 然后再到Dict(32)上访问:4→20→12→28。

这里可以看到大表的相关桶的顺序并非是按照之前所述的二进制高位序,实际上是按照低位序来遍历大表中高出小表的有效位。

大表t1高位都是从低位加1计算得出的,扫描的顺序却是从高位加1,向低位进位。Redis针对Rehashing时这种逻辑实现在扩容时是可以运行正常的,但是在缩容时高位序和低位序的遍历在大小表上的混用在一定条件下会出现问题。

再次示例,Dict的tablesize从32缩容到8:

  1. Dict(32) 从Cursor 0开始扫描;
  2. 准备扫描Cursor 20时发生Resize,缩容至原来的四分之一即tablesize为8,Rehashing;
  3. 客户端发起Cursor 20,首先访问Dict(8)中的4号桶;
  4. 再到Dict(32)上访问:20→28;
  5. 最后返回Cursor = 2。

可以看出大表中的12号桶没有被访问到,即遍历大表时,按照低位序访问会遗漏对某些桶的访问。

上述这种情况发生需要具备一定的条件:

  1. 在Dict缩容Rehash时Scan;
  2. Dict缩容至至少原Dict tablesize的四分之一,只有在这种情况下,大表相对小表的有效位才会高出二位以上,从而触发跳过某个桶的情况;
  3. 如果在Rehash开始前返回的Cursor是在小表能表示的范围内(即不超过7),那么在进行高位有效位的加一操作时,必然都是从0开始计算,每次加一也必然能够访问的全所有的相关桶;如果在Rehash开始前返回的cursor不在小表能表示的范围内(比如20),那么在进行高位有效位加一操作的时候,就有可能跳过 ,或者重复访问某些桶的情况。

可见,只有满足上述三种情况才会发生Scan遍历过程中漏掉了一些Key的情况。在执行清理Key的时候,如果清理的Key数量很大,导致了Redis内部的Hash表缩容至少原Dict tablesize的四分之一,就可能存在一些Key被漏掉的风险。

Scan源码优化

修复逻辑就是全部都从高位开始增加进行遍历,即大小表都使用高位序访问,修复源码如下:

unsigned long dictScan(dict *d,
                       unsigned long v,
                       dictScanFunction *fn,
                       dictScanBucketFunction* bucketfn,
                       void *privdata)
{
    dictht *t0, *t1;
    const dictEntry *de, *next;
    unsigned long m0, m1;

    if (dictSize(d) == 0) return 0;

    if (!dictIsRehashing(d)) {
        t0 = &(d->ht[0]);
        m0 = t0->sizemask;

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
        de = t0->table[v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Set unmasked bits so incrementing the reversed cursor
         * operates on the masked bits */
        v |= ~m0;

        /* Increment the reverse cursor */
        v = rev(v);
        v++;
        v = rev(v);

    } else {
        t0 = &d->ht[0];
        t1 = &d->ht[1];

        /* Make sure t0 is the smaller and t1 is the bigger table */
        if (t0->size > t1->size) {
            t0 = &d->ht[1];
            t1 = &d->ht[0];
        }

        m0 = t0->sizemask;
        m1 = t1->sizemask;

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
        de = t0->table[v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Iterate over indices in larger table that are the expansion
         * of the index pointed to by the cursor in the smaller table */
        do {
            /* Emit entries at cursor */
            if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
            de = t1->table[v & m1];
            while (de) {
                next = de->next;
                fn(privdata, de);
                de = next;
            }

            /* Increment the reverse cursor not covered by the smaller mask.*/
            v |= ~m1;
            v = rev(v);
            v++;
            v = rev(v);

            /* Continue while bits covered by mask difference is non-zero */
        } while (v & (m0 ^ m1));
    }

    return v;
}

我们团队已经将此PR Push到Redis官方: Fix dictScan(): It can't scan all buckets when dict is shrinking,并已经被官方Merge。

至此,基于Redis Rehash以及Scan实现中涉及Rehash的两个机制已经基本了解和优化完成。

总结

本文主要阐述了因Redis的Rehash机制踩到的两个坑,从现象到原理进行了详细的介绍。这里简单总结一下,第一个案例会造成线上集群进行大量淘汰,而且产生主从不一致的情况,在业务层面也会发生大量超时,影响业务可用性,问题严重,非常值得大家关注;第二个案例会造成数据清理无法完全清理,但是可以再利用Scan清理一遍也能够清理完毕。
注:本文中源码基于Redis 3.2.8。

作者简介

春林,2017年加入美团,毕业后一直深耕在运维线,从网络工程师到Oracle DBA再到MySQL DBA多种岗位转变,现在美团主要负责Redis运维开发和优化工作。

赵磊,2017年加入美团,毕业后一直从事Redis内核方面的研究和改进,已提交若干优化到社区并被社区采纳。

美团Squirrel技术团队,负责整个美团大规模分布式缓存Squirrel的研发和运维工作,支撑了美团业务快速稳定的发展。同时,Squirrel团队也将持续不断的将内部优化和发现的问题提交到开源社区,回馈社区,希望跟业界一起推动Redis健硕与繁荣。如果有对Redis感兴趣的同学,欢迎参与进来:hao.zhu#dianping.com。

Kafka系列(八)跨集群数据镜像

$
0
0

本系列文章为对《Kafka:The Definitive Guide》的学习整理,希望能够帮助到大家

在之前系列文章中,我们讨论了一个Kafka集群的搭建、维护和使用,而在实际情况中我们往往拥有多个Kafka集群,而且这些Kafka集群很可能是相互隔离的。一般来说,这些集群之间不需要进行数据交流,但如果在某些情况下这些集群之间存在数据依赖,那么我们可能需要持续的将数据从一个集群复制到另一个集群。而由于“复制”这个术语已经被用于描述同一集群内的副本冗余,因此我们将跨集群的数据复制称为数据镜像(Mirroring)。另外,Kafka中内置的跨集群数据复制器称为MirrorMaker。

跨集群数据镜像的用户场景

以下为跨集群数据镜像的一些典型用户场景:

  • 区域集群与中心集群:很多公司往往有多个数据中心,而且每个数据中心维护独立的Kafka集群。一般的应用可能只需要跟本地集群通信即可,但存在一些应用需要所有集群的数据。比如,一个公司在每个城市都有一个数据中心,并且该中心维护相应城市的产品供需数据以及价格数据;这些数据需要汇总到一个中心集群以便进行公司维度的营利分析。
  • 数据冗余:为了防止一个集群故障导致应用不可用,我们需要把数据同步到另一个集群,这样当一个集群出现故障,可以把应用的流量切换到备份集群。
  • 云迁移:一般公司都维护有自己的数据中心,但随着云设施越来越便宜,很多公司会选择将服务迁移到云上。数据迁移与复制也是其中一个重要部分,我们可以使用Kafka Connect将数据库更新同步到本地Kafka集群,然后再把数据从本地Kafka集群同步到云上的Kafka集群。

多集群架构

上面列举了多集群的用户场景,现在来看下多集群的常见架构。但在讨论架构前,先来了解跨集群通信的一些现实因素。

跨集群通信的现实因素

  • 高延迟:由于集群间的距离较长以及网络拓扑节点增多,集群的通信延迟也会增加。
  • 带宽有限:广域网(WAN)带宽通常比机房内带宽要小得多,并且可用带宽可能无时无刻都在变化。
  • 高成本:无论是自己维护的集群还是云上的集群,集群间通信的成本都是非常高的。这是因为带宽有限并且增加带宽会带来昂贵的成本,而且服务提供商对于跨集群、跨区域、跨云的数据传输会额外收取费用。

Kafka的broker和生产者/消费者客户端都是基于一个集群来进行性能调优的,也就是说在低延迟和高吞吐的假设前提下,经过测试与验证从而得到了Kafka的超时和缓冲区默认值。因此,一般我们不推荐同一个集群的不同broker处于多个数据中心。大多数情况下,由于高延迟和网络错误,最好避免生产数据到另一个集群。当然,我们可以通过提高重试次数、增加缓冲区大小等手段来处理这些问题。

这么看,broker跨集群、生产者-broker跨集群这两种方案都被否决了,那么对于跨集群数据镜像,我们只剩下一种方案:broker-消费者跨集群。这种方案是最安全的,因为即便存在网络分区导致消费者不能消费数据,这些数据仍然保留在broker中,当网络恢复后消费者仍然可以读取。也就是说,无论网络状况如何,都不会造成数据丢失。另外,如果存在多个应用需要读取另一个集群的数据,我们可以在每个数据中心都搭建一个Kafka集群,使用集群数据镜像来只同步一次数据,然后应用从本地集群中消费数据,避免重复读取数据浪费广域网带宽。

下面是跨集群架构设计的一些准则:

  • 每个数据中心都应该至少有一个Kafka集群;
  • 集群间尽可能只同步一次数据;
  • 跨集群消费数据由于跨集群生产数据。

中心集群架构

下面是多个本地集群和一个中心集群的架构:

hub-spoke

简单情况下只存在两个集群,即主集群和副本集群:

simple-hub-spoke

这种架构适用于,数据分布在多个数据中心而某些应用需要访问整体数据集。另外每个数据中心的应用可以处理本地数据,但无法访问全量数据。这种架构的主要优点在于,数据生产到本地,而且跨集群只复制一次数据(到中心集群)。只依赖本地数据的应用可以部署在本地集群,而依赖多数据中心的应用则部署在中心集群。这种架构也非常简单,因为数据流向是单向的,这使得部署、运维和监控非常容易。

它的主要缺点在于,区域的集群不能访问另一个集群的数据。比如,我们在每个城市维护一个Kafka集群来保存银行的用户信息和账户历史,并且将这些数据同步到中心集群以便做银行的商业分析。当用户访问本地的银行分支网站时,这些请求可以被分发到本地集群处理;但如果用户访问异地的银行分支网站时,要么该异地集群跟中心集群通信(此种方式不建议),要么直接拒绝请求(是的非常尴尬)。

多活架构

这种架构适用于多个集群共享数据,如下所示:

active-active

此架构主要优点在于,每个集群都可以处理用户的任何请求并且不阉割产品功能(与前一种架构对比),而且就近处理用户请求,响应时间可以大大降低。其次,由于数据冗余与弹性控制,一个集群出现故障,可以把用户请求导流到别的集群进行处理。

此架构主要缺点在于,由于多个集群都可以处理用户请求,异步的数据读取和更新无法保证全局数据一致性。下面列举一些可能会遇到的挑战:

  • 如果用户发送一个事件到一个集群,然后从其他集群读取事件信息,那么由于事件复制延迟,很有可能读取不到该事件。比如,用户添加一本书到心愿单后,访问心愿单却看不到添加的书。为了解决这个问题,研发人员可能会将用户与集群进行绑定,使用同一个集群来处理用户请求(当然在集群故障情况下会转移)。
  • 一个集群包含用户订购A的事件,另一个集群包含用户订购B的事件,而且这两个事件是几乎同时的。经过数据镜像后,每个数据中心都有这两个事件,而这两个事件可能是冲突的。我们需要决定哪个事件才是目前正确的最终事件么?如果需要,那么我们得制定规则来使得多个集群的应用都能得出相同的结论。或者我们可以认为这两个事件都是正确的,认为用户同时订购了A和B。亚马逊以前采取这种方式来处理冲突,但像证券交易这种机构不能采取这种方式。这个问题的解决方案是因地制宜的,我们需要知道的是一旦采取这种架构,冲突是无法避免的。

如果我们找到多集群异步读写的数据一致性问题,那么这种架构是最好的,因为它是可扩展的、弹性的,并且相对于冷热互备来说性价比也不错。

多活架构的另一个挑战是,如果存在多个数据中心,那么每一对中心都需要通信链路。也就是说,如果有5个数据中心,那么总共需要部署20个镜像进程来处理数据复制;如果考虑高可用,那么可能需要40个。

另外,我们需要避免事件被循环复制和处理。对于这个问题,我们可以将一个逻辑概念的主题拆分成多个物理主题,并且一个物理主题与一个数据中心对应。比如,users这个逻辑主题可以拆分成SF.users和NYC.users这两个物理主题,每个主题对应一个数据中心;NYC的镜像进程从SF的SF.users读取数据到本地,SF的镜像进程从NYC的NYC.users读取数据到本地。因此每个事件都只会被复制一次,而且每个数据中心都包含SF.users和NYC.users主题,并且包含全量的users数据。消费者如果需要获取全量的users数据,那么需要消费所有本地.users主题的数据。

需要提醒的是,Kafka正在计划添加记录头部,允许我们添加标记信息。我们在生产消息时可以加上数据中心的标记,这样也可以避免循环数据复制。当然,我们也可以自己在消息体中增加标记信息进行过滤,但缺点是当前的镜像工具并不支持,我们得自己开发复制逻辑。

冷热互备架构

有时候,多集群是为了防止单点故障。比如说,我们可能有两个集群,其中集群A处于服务状态,另一个集群B通过数据镜像来接收集群A所有的事件,当集群A不可用时,B可以启动服务。在这种场景中,集群B包含了数据的冷备份。架构如下所示:

active-standby

这种架构的优点在于搭建简单并且适用于多种场景。我们只需搭建第二个集群,设置一个镜像进程来将源集群的所有事件同步到该集群即可,并且不用担心发生数据冲突。缺点在于,我们浪费了一个集群资源,因为集群故障通常很少发生。一些公司会选择搭建低配的备份集群,但这样会存在一个风险,那就是无法保证出现紧急情况时该备份集群是否能支撑所有服务;另一些公司则选择适当利用备份集群,那就是把一些读取操作转移到备份集群。

集群故障转移也具有一些挑战性。但无论我们选择何种故障转移方案,SRE团队都需要进行日常的故障演练。因为,即便今天故障转移是有效的,在进行系统升级之后很可能失效了。一个季度进行一次故障转移演练是最低限度,强大的SRE团队会演练更频繁,Netflix著名的Chaos Monkey玩的更溜,它会随机制造故障,也就是说故障每天都可能发生。

下面来看下故障转移比较具有挑战性的地方。

数据损失与不一致

很多Kafka的数据镜像解决方案都是异步的,也就是说备份集群不会包含主集群最新的消息。在一个高并发的系统中,备份集群可能落后主集群几百甚至上千条消息。假如集群每秒处理100万条消息,备份集群与主集群之间有5ms的落后,那么在理想情况下备份集群也落后将近5000条消息。因此,我们需要对故障转移时的数据丢失做好准备。当然在故障演练时,我们停止主集群之后,可以等待数据镜像进程接收完剩余的消息,再进行故障转移,避免数据丢失。另外,Kafka不支持事务,如果多个主题的数据存在关联性,那么在数据丢失的情况下可能会导致不一致,因此应用需要注意处理这种情况。

故障转移的开始消费位移

在故障转移中,其中一个挑战就是如何决定应用在备份集群的开始消费位移。下面来讨论几个可选的方案。

  • 自动位移重置:Kafka消费者可以配置没有已提交位移时的行为,要么从每个分区的起始端消费,要么从每个分区的最末端消费。如果我们的消费者提交位移到Zookeeper,而且没有对Zookeeper中的位移数据进行镜像备份,那么我们需要从这两个选项中做出选择。选择从起始端开始消费的话,可能会存在大量重复的消息;选择从最末端消费的话,可能会存在消息丢失。如果这两种情况可以忍受的话,那么建议选择这种方案,因为这种方案非常简单。
  • 复制位移主题:如果我们使用0.9或者更高版本的Kafka消费者,消费者会提交位移到一个特殊的主题,_consumer_offsets。如果我们复制这个主题到备份集群,那么备份集群的消费者可以从已提交的位移处开始消费。这种方案也很简单,但是有一些情况需要注意。首先,主集群和备份集群的消息位移不能保证是一样的。举个例子,我们在主集群中只保留3天的数据,在主题创建并且使用了一个星期之后,我们开始进行备份集群的数据镜像;在这个场景中,主集群的最新消息位移可能到达57000000,而备份集群的最新消息位移是0,并且由于主集群中老的数据已经被过期删除了,备份集群的消息位移跟主集群始终是不一样的。其次,即便我们在创建主题就进行数据镜像,由于生产者失败重试,仍然会导致不同集群的消息位移是不同的。最后,即便主集群和备份集群的消息位移完全一致,由于主集群和备份集群存在一定的消息落后并且Kafka不支持事务,消费者提交的消息位移可能在相应消息之前或之后到达。因此,在故障转移时消费者可能根据位移找不到匹配的消息,或者位移落后于主集群。总的来说,如果备份集群的提交位移比主集群的提交位移更老,或者由于重试导致备份集群的消息比主集群的消息多,那么会存在一定的数据重复消费;如果备份集群的提交位移没有匹配到相应的消息,那么我们可能仍然需要从主题起始端或者最末端进行消费。因此,这种方案能够减少数据重复消费或者数据丢失,但也不能完全避免。
  • 基于时间的故障转移:如果我们使用0.10.0或者更高版本的Kafka消费者,每条消息都会包含发送到Kafka的时间戳。而且,0.10.1.0或者更高版本的broker会建立一个索引,并且提供一个根据时间戳来查询位移的API。因此,假如我们知道故障在某个时间发生,比如说为早上4:05,那么我们可以让备份集群的消费者从早上4:03处开始消费数据,虽然这样会有两分钟的数据重复消费,但至少数据没有丢失。这个方案的唯一问题是,我们怎么告诉备份集群的消费者从特定时间点开始消费呢?一个解决思路是,我们在应用代码中支持指定开始消费的时间,然后使用API来获取该时间对应的位移,然后从该位移处开始消费处理。但如果应用代码没有支持这种功能,我们可以自己写一个小工具,该工具接收一个时间戳,然后使用API来获取所有主题分区的位移,最后提交这些位移,这样备份集群的消费组在启动时会自动获取位移,然后进行消费处理。这种方案是最优的。
  • 外部位移映射:在上面讨论复制位移主题的时候,曾提到一个最大的挑战是主集群和备份集群的消息位移不一致。基于这个问题,一些公司选择开发自己的数据镜像工具,并且使用外部存储系统来存储集群间的消息位移映射。比如,主集群中位移为495的消息对应于备份集群中位移为500的消息,那么在外部存储系统中记录(495,500),这样在故障转移时我们可以基于主集群的已提交位移和映射来得到备份集群中的提交位移。但这种方案没有解决位移比消息提前到达备份集群的问题。这种方案比较复杂,升级集群然后使用基于时间的故障转移可能更便捷。

故障转移之后

假如故障顺利转移到备份集群,并且备份集群正常工作,那么原主集群应该怎么处理呢?可能需要将其转化为备份集群。你可能会想,能不能简单修改数据镜像工具,让其换个同步方向,从新的主集群同步数据到老的主集群?这样会导致两个问题:

  • 我们如何得知从什么地方开始进行数据镜像呢?这个问题跟故障转移时消费者不知道消费位移的问题是一样的,而且解决方案也会存在消息重复或者丢失的问题。
  • 如前所述,老的主集群可能会包含备份集群没有同步的数据更新,如果只是简单的将新主集群的数据同步回来,那么这两个集群又会发生不一致的情况。

因此,最简单的解决方案是,清除老主集群的所有状态和数据,然后重新与新主集群进行数据镜像,这样可以保证这两个集群的状态是一致的。

其他事项

故障转移还有一个需要注意的地方是,应用如何切换与备份集群进行通信?如果我们在代码中直接硬编码主集群的broker,那么故障转移比较麻烦。因此,很多公司会创建一个DNS名称来解析到主集群的broker,当故障转移时将DNS解析到备份集群的broker。由于Kafka客户端只需要成功连接到集群的一个broker便可通过该broker发现整个集群,因此我们创建3个左右的DNS解析到broker即可。

延伸集群

延伸集群主要用来防止单个数据中心故障导致Kafka服务不可用,其解决方案为:将一个Kafka集群分布在多个数据中心。因此延伸集群与其他集群方案有本质的区别,它就是一个Kafka集群。在这种方案中,我们不需要数据镜像来同步,因为Kafka本身就有复制机制,并且是同步复制的。在生产者发送消息时,我们可以通过配置分区机架信息、min.isr、acks=all来使得数据写入到至少两个数据中心副本后,才返回成功。

这种方案的优点是,多个数据中心的数据是实时同步的,而且不存在资源浪费问题。由于集群跨数据中心,为了得到最好的服务性能,数据中心间需要搭建高质量的通信设施以便得到低延迟和高吞吐,部分公司可能无法提供。

另外需要注意的是,一般需要3个数据中心,因为Kafka依赖的Zookeeper需要奇数的节点来保证服务可用性,只要有超过一半的节点存活,服务即可用。如果我们只有两个数据中心,那么肯定其中一个数据中心拥有多数的Zookeeper节点,那么该数据中心发生故障的话服务便不可用;如果拥有三个数据中心并且Zookeeper节点均匀分布,那么其中一个数据中心发生故障,服务仍然可用。

MirrorMaker

Kafka内置了一个用于集群间做数据镜像的简单工具–MirrorMaker,它的核心是一个包含若干个消费者的消费组,该消费组从指定的主题中读取数据,然后使用生产者把这些消息推送到另一个集群。每个消费者负责一部分主题和分区,而生产者则只需要一个,被这些消费者共享;每隔60秒消费者会通知生产者发送消息数据,然后等待另一个集群的Kafka接收写入这些数据;最后这些消费者提交已写入消息的位移。MirrorMaker保证数据不丢失,而且在发生故障时不超过60秒的数据重复。内部架构如下所示:

MirrorMaker

如何配置

首先,MirrorMaker依赖消费者和生产者,因此消费者和生产者的配置属性对MirrorMaker也适用。另外,MirrorMaker也有自身的属性需要配置。先来看一个配置的代码样例:

bin/kafka-mirror-maker --consumer.config etc/kafka/consumer.properties --producer.config etc/kafka/producer.properties --new.consumer --num.streams=2 --whitelist ".\*"
  • consumer.config:这个配置文件指定了所有消费者的属性,其中bootstrap.servers属性指定了源集群,group.id指定了所有消费者的使用的消费组ID。另外,auto.commit.enable=false这个配置最好不要更改,因为MirrorMaker根据消息是否写入目标集群来决定是否提交位移,修改此属性可能会造成数据丢失。auto.offset.reset这个属性默认为latest,也就是说创建MirrorMaker时会从该时间点开始数据镜像,如果需要对历史数据进行数据镜像,可以设置成earliest。
  • producer.config:这个配置文件指定了MirrorMaker中生产者的属性,其中bootstrap.servers属性指定了目标写入集群。
  • new.consumer:MirrorMaker可以使用0.8版本或者新的0.9版本消费者,建议使用0.9版本消费者。
  • num.streams:指定消费者的数量。
  • whitelist:使用正则表达式来指定需要数据镜像的主题。上面的例子中指定对所有的主题进行数据镜像。

在生产环境中部署MirrorMaker

上面的例子展示了如何使用命令行启动MirrorMaker,当在生产环境中部署MirrorMaker时,你可能会使用nohub和输出重定向来将使得它在后台运行,不过MirrorMaker已经包含-daemon参数来指定后台运行模式。很多公司都有自己的部署运维系统,比如Ansible,Puppet,Chef,Salt等等。一个更为高级的部署方案是使用Docker来运行MirrorMaker,而且越来越流行。MirrorMaker本身是无状态的,不需要任何磁盘存储,并且这种方案可以使一台机器运行多个MirrorMaker(也就是说运行多个Docker)。对于一个MirrorMaker来说,它的吞吐瓶颈在于只有一个生产者,因此使用多个MirrorMaker可以提高吞吐,而使用Docker部署多个MirrorMaker尤其方便。另外,Docker也可以支持业务洪峰低谷的弹性伸缩。

如果允许的话,建议将MirrorMaker部署在目标集群内,这是因为如果一旦发生网络分区,消费者与源集群断开连接比生产者与目标集群断开连接要安全。如果消费者断开连接,那么只是当前读取不到数据,但是数据仍然在源集群内,并不会丢失;而生产者断开连接,MirrorMaker便生产不了数据,如果MirrorMaker本身处理不当,可能会丢失数据。

但对于在集群间需要加密传输数据的场景来说,将MirrorMaker部署在源集群也是个可以考虑的方案。这是因为在Kafka中使用SSL进行加密传输时,消费者相比生产者来说性能受影响更大。因此我们可以在源集群内部broker到MirrorMaker的消费者间不使用SSL加密,而在MirrorMaker跨集群生产数据时使用SSL加密,这样可以将SSL的性能影响降到最低。另外,尽量配置acks=all和足够的重试次数来降低数据丢失的风险,而且如果MirrorMaker一旦发送消息失败最好让其暂时退出,避免丢失数据。

为了降低目标集群和源集群的消息延迟,建议将MirrorMaker部署在两台不同的机器上并且使用相同的消费组,这样一台发生故障另外一台仍然可以保证服务正常。

在生产环境中部署MirrorMaker时,监控是很重要的,下面是一些重要的监控指标:

延迟监控

延迟是指目标集群与源集群的消息落后间隔,间隔值通过计算源集群最新的消息与目标集群最新的消息来得到。下图中源集群最新的消息位移是7,目标集群最新的消息位移是5,延迟间隔为2。

lag

有两种方式来监控此指标,但各有优缺点:

  • 检测MirrorMaker提交到源集群的位移。我们可以使用kafka-consumer-groups来检测分区的最新位移以及MirrorMaker提交的位移,通过计算差值得到落后间隔。但这种计算方式不是100%准确的,因为MirrorMaker不是时刻提交位移的,默认情况下每分钟提交一次位移。因此你可能会看到间隔在一分钟内逐渐增长,然后突然降低。拿上面的例子来说,实际的间隔为2,但由于MirrorMaker没有提交位移,kafka-consumer-groups工具可能会检测到落后间隔为4。LinkedIn的Burrow工具相对来说更成熟,可以避免这种问题。
  • 检测MirrorMaker读取到的消息位移(可能还没有提交)。MirrorMaker的消费者会通过JMX来发布指标,其中一个指标就是消费者落后间隔(聚合所有分区)。但这个间隔也不是100%准确的,因为它是根据消费者读取到的位移来计算的,并没有考虑是否已经写入目标集群。拿上面的例子来说,MirrorMaker消费者可能会汇报落后间隔为1而不是2,因为它已经读取到消息6,即便这个消息仍未写入到目标集群。

指标监控

MirrorMaker中包含消费者和生产者,它们都有许多指标,建议在生产环境中收集跟踪这些指标。 Kafka文档列举了所有可用的指标,下面是一些比较重要的指标:

  • 消费者:fetch-size-avg, fetch-size-max, fetch-rate, fetch-throttle-time-avg, 和fetch-throttle-time-max。
  • 生产者:batch-size-avg, batch-size-max, requests-in-flight,和record- retry-rate。
  • 消费者和生产者:io-ratio和io-wait-ratio。

Canary

如果你已经监控了所有的指标,那么Canary不是必须的。但我们仍然推荐在生产环境中使用Canary,因为它能提供整体的监控。Canary每分钟发送一个事件到源集群,然后尝试从目标集群读取该事件,如果时间间隔超过阈值就会发出报警信息,因为这意味着MirrorMaker数据镜像存在问题。

MirrorMaker性能调优

首先MirrorMaker的集群大小需要依赖所需要满足的吞吐量和延迟。如果不能忍受延迟,那么你可能需要尽可能部署多的MirrorMaker以便处理流量洪峰;如果能忍受一定的延迟,那么MirrorMaker处理洪峰的75%-80%或者95%-99%就可以了,洪峰的延迟会在低谷时慢慢降低。

现在我们来评估MirrorMaker的消费者线程数,也就是num.streams所指定的值。LinkedIn的经验值是8个消费者线程可以达到6MB/s的处理速度,16个消费者线程可以达到12MB/s的速度,但这个经验值不是通用的,因为它受硬件配置影响。因此我们需要自己做压力测试,Kafka中内置有kafka-performance-producer,可以使用它作为生产者来发送压测事件到源集群,然后测试MirrorMaker在1,2,4,8,16,24,32个线程下的性能,当增加线程数不能提高性能时即取得极值,配置的线程数需要小于这个极值即可。如果我们发送的消息是经过压缩的,那么MirrorMaker的消费者需要解压然后生产者重新压缩,这个过程会消耗CPU,因此在测试过程中也需要关注CPU负载情况。这个过程可以测试单个MirrorMaker的性能,如果以集群形态部署,那么我们需要对多个MirrorMaker的集群进行性能压测。

另外,核心的主题可能需要尽可能降低延迟,对于这种情况建议在部署MirrorMaker时进行隔离,防止别的大流量主题影响到核心主题。

上面是基本的性能调优,一般能满足业务需求了。但我们其实还可以进一步提高MirrorMaker的性能。在使用MirrorMaker做跨集群数据镜像时,我们可以对网络参数进行性能调优:

  • 增大TCP缓冲区(net.core.rmem_default, net.core.rmem_max, net.core.wmem_default, net.core.wmem_max, net.core.optmem_max)。
  • 使用自动滑动窗口(sysctl –w net.ipv4.tcp_window_scaling=1,或者添加net.ipv4.tcp_window_scaling=1到/etc/sysctl.conf)。
  • 降低TCP慢启动时间(设置/proc/sys/net/ipv4/ tcp_slow_start_after_idle为0)

网络性能调优是一个复杂的过程,感兴趣的可以参考《Performance Tuning for Linux Servers》这本书。

另外,如果需要对MirrorMaker的生产者和消费者进行性能调优的话,我们得首先了解性能瓶颈究竟是在于生产者还是消费者。一个方法是监控生产者和消费者指标,如果发现一个空闲而另一个负载非常高,那么就知道瓶颈在哪了。或者我们可以使用jstack来对线程栈进行多次采样,看MirrorMaker究竟主要耗费时间在poll消息还是send消息,然后再进行优化。

如果想优化生产者,那么下面是一些比较重要的属性配置:

  • max.in.flight.requests.per.connection:默认情况下,MirrorMaker生产者同时只发送一个请求,这意味着生产者等到目标集群ack后才发送下一个请求。这种方式可以保证在失败重试的情况下仍然保持消息顺序。不过如果集群间通信延迟较大,这种方式会降低发送性能,因此对于消息顺序不重要的场景,我们可以通过增加max.in.flight.requests.per.connection来提高吞吐。
  • linger.ms和batch.size:如果检测到生产者发送的消息经常是很小的(比如说batch-size-avg和batch-size-max都小于配置的batch.size),那么我们可以通过增加linger.ms来让生产者等待更多的消息然后再发送请求,但注意到这种方式也会增加延迟。而如果观测到生产者每次发送的消息都是满足batch.size的,而我们又有空余的内存,那么可以考虑增大batch.size。

如果想优化消费者,下面是一些比较重要的属性配置:

  • partition.assignment.strategy:MirrorMaker默认的分区均衡策略为range,这种方式有一定的好处,但是可能会导致分区不均衡分配。对于MirrorMaker来说,我们可以考虑设置成轮询策略(Round Robin),只需要将partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor添加到配置文件即可。
  • fetch.max.bytes:如果检测到fetch-size-avg和fetch-size-max都跟fetch.max.bytes很接近,而我们又有空余的内存空间,那么可以考虑fetch.max.bytes来使得消费者在每个请求中读取更多的数据。
  • fetch.min.bytes和fetch.max.wait:如果检测到fetch-rate指标很高,那么证明消费者频繁拉取消息,而且拉取的消息非常少,那么我们可以考虑增加fetch.min.bytes和fetch.max.wait来使得消费者每次可以等待拉取更多的消息。

其他跨集群数据镜像解决方案

上面深入讨论了MirrorMaker的方案,但如前所述MirrorMaker有自身的局限性和缺点,下面来看下MirrorMaker的替代方案以及它们是如何解决MirrorMaker所遇到的问题的。

Uber uReplicator

Uber大规模使用MirrorMaker,随着主题增多和集群规模增长,他们遇到了一些问题:

  • 重平衡延迟:MirrorMaker内部的消费者只是普通的Kafka消费者,因此增加消费者线程、增加MirrorMaker实例、增加主题等等都会引起分区的重平衡。在前面系列文章提到过,分区重平衡进行时,消费者不能消费数据,直至重平衡完毕。如果主题和分区数量很大,那么这个过程会需要一定时间,对于老的消费者来说时间则更长。在一些场景下,甚至会引起5到10分钟的停顿。
  • 新增主题困难:使用正则表达式来指定主题列表意味着每新增一个主题都会引起上面所说的重平衡,因此为了避免不必要的重平衡,Uber单独指明需要数据镜像的主题列表。但Uber在新增镜像主题时需要修改所有MirrorMaker的配置并且重启,这仍然会导致重平衡。不过这样可以控制重平衡次数,只是定期维护导致重平衡,而不是每次新增主题都进行重平衡。需要注意的是,如果配置出错导致MirrorMaker间的配置不同,那么MirrorMaker启动后会不断的重平衡,因为消费者间不能达成一致。

基于上述的问题,Uber开发了uReplicator来替代MirrorMaker,他们使用Apache Helix来管理分配到uReplicator的主题和分区,并且使用REST API来在Helix中新增主题。Uber使用自身研发的Helix消费者来替代MirrorMaker中的消费者,Helix消费者从Helix中获取分区,并且监听Helix的分区改动事件,以此来避免原生的消费者重平衡。

Uber写了一篇 博客来描述这个架构,并且详细说明了这种方案的改进之处。

Confluent Replicator

在Uber开发uReplicator的同时,Confluent公司也在开发Replicator。虽然这两者名称基本相同,但是它们的侧重点却是不一样的。Confluent公司的Replicator主要是解决商业上遇到的多集群部署维护问题:

  • 集群配置不一致:MirrorMaker只是同步数据,但是不同集群的主题配置(分区数、冗余因子等)可能是不同的。如果我们在源集群增加了主题数据的保留时间但忘记在目标集群修改相同的配置,可能会导致在故障转移时,应用找不到历史数据。而且,手动同步主题配置非常容易出错。
  • MirrorMaker集群本身维护困难:上面说到MirrorMaker一般是以集群来部署的,本身也需要维护。MirrorMaker除了配置生产者和消费者之外,本身也有许多属性需要配置。如果我们有多个数据中心需要相互同步数据,那么MirrorMaker数量会迅速膨胀。这些情况都导致了MirrorMaker集群的运维复杂性。

为了降低运维复杂性,Confluent公司研发了Replicator,它是Kafka Connect的一种connector,与从数据库读取的connector不同的是,Replicator从Kafka集群中读取数据。Kafka Connect框架中的connector会将整体工作拆分成多个task,其中每个任务是一对<consumer, producer>。Connect框架将task均衡分配到各个worker节点,因此我们不需要计算每个MirrorMaker需要多少个消费者或者一个机器上部署多少个MirrorMaker实例。另外,Connect提供了REST API来管理connector和task。通过使用使用基于Kafka Connect框架的方案,我们可以降低需要维护的集群数量。而且,Replicator除了同步数据之外,也会同步Zookeeper中的主题配置。

白话理解: 准确率(Accuracy), 精确率(Precision), 召回率(Recall)

$
0
0

本文重点是在白话,不是数学上面的严格定义. 那首先要有一个业务场景,就好比上学,学习数据库,就要用到学生成绩. 在这,我们的业务场景就是对100个西瓜进行分类(已知生熟各半)

 

下面是针对上面场景,对各个术语的解释

准确率(Accuracy): 对所有西瓜分类正确的比率.

精确率(Precision): 挑出来的熟西瓜,有多少是正确的.

召回率(Recall) : 50个熟西瓜,有多少被分来到熟西瓜这个类别.

 

下面我们来分析各个术语有什么应用场景:

Accuracy: 这个是我们最常用的,但是这个指标有一个缺点,就是当数据分来不均匀的时候,就没办法用于业务了. 比如, 当生西瓜只有2个,熟西瓜有98个的时候. 只要判断所有的都是熟西瓜,准确率就是98%.但是这个模型其实是不合理的.

 

Precision: 这个指标就是为吃瓜群众准备的了, 比如100个西瓜里面,我只需要挑选出2个西瓜,并且都是熟西瓜,那么这个Precision就是100%. 其他的西瓜,就可以都判断为生西瓜.

 

Recall: 这个指标就是为瓜农准备的了, 瓜农肯定是想100%把所有的熟西瓜挑出来,送到市场上卖, 有多少熟西瓜被挑选出来了,就是用Recall这个指标来衡量了.

 

如果是黑心瓜农,直接把所有的西瓜,都当成熟西瓜,那么Recall就是100%了. 想想宁可错杀一千,不能错过一个.就是只注意了Recall.

如果是良心瓜农的话,会兼顾Precision. 也就是尽可能的排除生西瓜.

 

 

下面是三个不同角色对应的场景举例:

  • 针对吃瓜群众, 只希望挑出2个熟西瓜.其他的不管(Precision=100%)
 

 

Precision=2/(2+0)100.00%
Recall=2/(2+48)4.00%
F1=2*1*0.04/(1+0.04)7.69%

 

 

  • 黑心瓜农,所有的熟西瓜一个也不能放过,至于有没有生西瓜混入,完全不管. 那就是可以把所有100个西瓜当成熟西瓜卖个消费者. (Recall = 100%)


 
Precision=50/(50+50)50.00%
Recall=50/(50+0)100.00%
F1=2*0.5*1/(0.5+1)66.67%

 

 

  • 良心瓜农,经验丰富,可以识别大部分的熟西瓜,当然也会有些失误混入了少量生西瓜.这个模型比较均衡, Precision 和Recall相对比较高. 



 

 

Precision=48/(48+2)96.00%
Recall=48/(48+2)96.00%
F1=2*0.96*0.96/(0.96+0.96)96.00%

 

 

 

各种指标的数学定义公式:



 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



图灵奖得主John Hennessy、David Patterson访谈:未来小学生都能做机器学习

$
0
0

雷锋网 AI 科技评论按:在 Google Cloud Next 2018 大会上有一个万众期待的环节,就是今年三月获得 2017 年图灵奖的 John L. Hennessy、 David A. Patterson 两人的现场访谈。

谷歌母公司 Alphabet 董事长、斯坦福大学前校长 John Hennessy 与谷歌 TPU 团队、UC 伯克利大学退休教授 David Patterson 两人因计算机体系架构的设计与评价方法以及对 RISC 微处理器行业的巨大影响获得图灵奖后,在多个场合进行了演讲。在计算机体系结构顶级学术会议 ISCA 2018 上,两人就是受邀嘉宾,面对自己研究领域内的研究人员们进行了一场严肃、详细而富有前瞻性的学术演讲。雷锋网 AI 科技评论的报道请看 这里

而在谷歌云 Next 2018 大会上,谷歌云 CEO Diane Greene 作为主持人,与两人展开了一场面向普通大众的、覆盖话题更为广泛的现场访谈。访谈氛围不仅轻松幽默,也展现了两人对整个计算机领域的独到观点。雷锋网 AI 科技评论把访谈内容整理如下。

Diane:我知道大家都很期待这个两位大牛的访谈环节。我先简单介绍一下 David 和 John,我想大家都已经认识他们了,不过还是啰嗦一下,John Hennessy 是 Alphabet 的董事长,David Patterson 现在是谷歌的杰出工程师(distinguished engineer)。两人合著了大多数人学习计算机硬件架构所用的教科书(《计算机体系结构(量化研究方法)》,Computer Architecture: A Quantitative Approach),这本书现在也已经发行了第二版了。他们共同开发了 RISC 架构,也在今年获得了图灵奖,油管上有一个很棒的主题演讲。图灵奖的颁奖词是二人「 开创了一种系统的、定量的方法来设计和评价计算机体系结构,并对 RISC 微处理器行业产生了持久的影响」。

1997 年的时候 John 来到斯坦福大学任教授,1981 年开始研究 MIPS 项目。1989 到 1993 年 John 任计算机系统实验室主任 —— 其实一般介绍的时候是不用说这一句的,但是很巧的是我的丈夫就是在这个时候被招到斯坦福去的,我在斯坦福跟他认识的。然后 2000 年到 2016 年的 16 年间 John 担任斯坦福大学校长,对斯坦福大学有非常大的影响。

David 1976 年加入 UC 伯克利大学任计算机科学系的教授,从 1980 年开始担任 RISC 项目的领导者之一。我就是在这里跟 David 认识的,是他的计算机架构课上的学生 —— 我猜他已经不记得我了( - David:当然记得啊。 - John:那她拿到 A 了吗? - David:A+,肯定是 A+)。David 2016 年退休,在 UC 伯克利工作了 40 年。两个人都培养了许多优秀的学生。我刚刚才发现 Wikipedia 上写了 David 在一个每次 2 天的自行车骑行慈善活动里从 2006 一直到 2012 年都是最高贡献者,看来后来没有继续骑了。

两位除了为这个世界作出了很大贡献之外,还有一件很棒的事情是, 两个人都与同一个妻子结婚超过 40 年。(全场哄堂大笑)

John:如果换了妻子那就肯定不算了啊!

David:澄清一下,我们是各自和各自的妻子结婚,不是娶了同一个人……(全场再次大笑) 谣言就是这么产生的……

(大笑过后)

Diane:那么,你们两个人都不是在硅谷附近长大的,上大学是在 70 年代,拿到博士学位要到 70 年代中末了。你们肯定在这之前就对电子电气工程和计算机科学感兴趣了,虽然这是很久以前的事情了,不过当时都是因为什么原因呢?

David:我先说吧,我当时在 UCLA 念数学专业,这时候大学都还没有开设计算机专业。那时候我已经知道世界上有计算机这种东西了,但我从来没有想过要做计算机行业的事情,也没有什么毕业生劝我去做计算机。大三下学期有一门数学课取消了,我就在 UCLA 旁听了半门计算机的课程,当时讲的是 Fortran 语言,用的还是打孔纸卡,不过这都不重要,重要的是就是那个时候计算机来到了我的脑海里,我被深深地迷住了。我在大四的商务和工程课的课后自学了各种计算机相关的课程,然后毕业前有人给了我一份实验室的活干,我也就这样跟着读研了。

John:我第一次接触电脑是在上高中的时候,那时候我们有时分共享的计算机和纸带,现在看起来很奇怪的。我参与了一个科学项目,要做一台能玩三连棋(tic-tac-toe)的机器,然后用的都是继电器,现在的人很难想象,但是当时我也就只买得起这个。玩三连棋的人都知道,你稍微聪明点就能赢,但是很多人其实没那么聪明,所以机器还挺经常赢的。不过也就靠这个,我说服了我当时的女朋友她们一家人觉得我做这个也能做得下去,然后她后来成了我的妻子,所以从这个角度来看结果还算不错。

然后到了我上大学的时候,现在的人肯定不会相信 70 年代的时候是没法学计算机专业的,有一些计算机的研究生专业,但是没有本科专业。所以我学的是电子电气工程,也决定好了要继续读计算机的研究生,就这样不后悔地一路过来了。

Diane:这几十年过得很快,如果当时有人告诉你们未来的技术发展是怎么样的,你们会觉得惊讶吗?

David:应该会吧。你知道 Gordon Moore (英特尔创始人之一,摩尔定律提出者)当时在 IEEE 的某个期刊发了一篇文章写了他对未来的预测,他画了几张画,画了未来的计算机和小汽车,还有计算机可以在商店里面买卖。实话说我当时是不太相信他的预测的。

John:对的,有一张画里画的就是有人在销售家用的计算机。不过即便 Gordon 对未来也有一些犹豫,他说我们能预测未来 10 年会发生什么,但是更远的就预测不了了。所以我觉得没有人能想到,谁能想到微处理器最终占据了整个计算机产业,完全代替了大型机和超级计算机,谁又能想到大数据、机器学习会像现在这样爆发呢?

David:是这样。机器学习其实一直以来都有,但它的突然爆发、不停登上媒体头条也就是过去四五年的事情。即便只是在 10 年前都很难预测到这样的发展。

Diane:确实很惊人。那么,说到摩尔定律,摩尔定律现在怎么样了呢?

David结束了!摩尔定律结束了!人们很难相信摩尔定律终结了是因为十年、二十年以前就有人说摩尔定律要终结了,但现在是真的来了。摩尔定律是说,每一到两年晶体管的数量就要翻倍,在我们的演讲里 John 有一页 PPT 上就有一张图,这个翻倍的趋势已经结束了。这不代表以后不会有新技术带来新的提升了,不代表我们停滞不前了,但是确实不是每一两年就翻番了,它的速度要慢得多了。

John:对的。最近 5 年里这个减速的趋势变得非常明显,这 5 年里的发展速度已经比摩尔定律预测的速度要少了 10 倍,而这样的缓慢发展的趋势还会继续。另外还有一个大家讨论得不那么多的、但是实际上要更尖锐的问题是 Dennard 缩放定律。Robert Dennard 开发的技术大家都在使用,就是把单个晶体管作为 DRAM 的一个单元,我们每个人每天都在用。 他做了一个预测说,单位平方毫米的晶体管所消耗的能源是保持固定的,也就是说,随着技术的发展,单位计算能力所消耗的能源会下降得非常快。这个预测的依据是电压缩放定律等等,但是 Dennard 缩放定律现在也失效了。这就是你看到现在的处理器需要降低频率、关闭一些核心来避免过热的原因,就是因为 Dennard 缩放定律失效了。

David:第三代的 TPU 已经是水冷的了,就是这个原因。

John:对于大型数据中心来说水冷没什么不好,但是手机总不能也用水冷吧,我还得背一个包,包上都是散热片。那也太滑稽了。

Diane:成了比能源的游戏。

John:对,比的是能源了。

Diane:很有趣。那么继续这个处理器的话题,你们一个人做了 RISC,一个人做了 MIPS,那你们当时做芯片花了多久,为什么要做呢?这个问题挺大了的了。

David:最早我们在 UC 伯克利开始了 RISC 的研究。RISC 是指精简指令集计算机。我们不仅讨论这个想法讨论了很久,我们也构建了模拟器和编译器。我们两个人都做了芯片的实物出来。

RISC 的想法并不难,它的出发点是,软件要借助一定的词库和硬件沟通,这个词库就被称作「指令集」。大个头的计算机里占据了支配地位的思想是要有一个很大、很丰富的词库,可能有好几千个词,别的软硬件要使用起来都比较方便。John 和我的想法与此刚好相反,我们觉得要有一个简化的词库、简单的指令集。那我们面对的问题就是,除此之外你还需要处理多少指令集、你处理它们的速度又有多快。最后我们的研究结果是,比我们一开始的计划增加了 25% 的指令集,但我们读取的速度增加到了 5 倍快。1984 年在旧金山,斯坦福的学生和 UC 伯克利的学生一起在最顶级的会议上发表了科研成果,我们拿出的芯片无可争议地比工业界现有的芯片好得多。

Diane:从你们产生想法,到做出芯片发表,花了多长时间?现在做类似的事情需要花多久?

David:4 年。现在花的时间肯定要少很多了。

John:从当时到现在,很多东西都在同时变化。微处理刚刚出现的时候,人们都是用汇编语言写程序,然后随着逐步切换到高级语言,大家开始关注有什么编译器可以用,而不是有哪些汇编语言的好程序可以用。UNIX 也就是那个时候出现的,我们开始有用高级语言写的操作系统,整个技术环境都在变化,当时虽然也有位片式的计算机,但是微处理器是一种新的技术,有着新的取舍。所有这些东西都给技术发展带来了新的起点,设计计算机也开始换成新的思路。

Diane:那么你们的想法被接受了吗?

(David 和 John 两人大笑)

David:大家都觉得我们是 满大街扔鸡尾酒燃烧瓶的激进分子。当时占据统治地位的想法就是,很丰富的指令集对软件来说更有帮助。要回到更简单、更原始的指令集,很多软件都会出问题。别人都觉得我们这是很危险的点子。1980 到 1984 年间,我们去那些大规模的会议参与讨论的时候,好几次别人直接生气然后开始大叫。我和 John 两个人在一方,其他嘉宾、会场里所有其他的人都在我们的对面。过了几年以后,他们才逐渐开始接受我们的观点。

John:不过工业界总是很抗拒。我记得当时有一个著名的计算机先驱对我说,你会遇到的麻烦是,你影响到了他们的现有利润了,因为你的技术的性价比要高得多,他们卖 20 万美元的小型计算机,就要被你的 2.5 万美元的小盒子代替了。对他们来说简直是毁灭性的。很多人都担心这个。也有很多人不相信这会发生,但是最后就是这样发生了。

David:今天都有很多人不认为 RISC 有什么好处。(笑)

Diane:在你们开发 RISC 的时候,Intel 也发展得很快。

John:Intel 做了很多事情。首先他们发现了一种非常聪明的方式实现一种叫做 SIS 的指令集,它可以把 x86 的指令集转换成 RISC 指令集,然后构建出 RISC 指令集的工作流水线。他们确实这样做了,在 Pentium Pro 上很高效地实现了它,在效率方面带来了很大的改进。对于芯片来说,缓存占的面积越来越大,其它的东西变得不那么重要了。但是有那么一个问题是你没法克服、也没法绕过的,就是 芯片的设计开销以及设计时间。对 Intel 来说没什么问题,他们的开发周期是 2 到 3 年,有四百名工程师的开发团队。但是这个世界上还有很多别的企业,比如设计移动设备的企业,你可能需要有 5 款不同的芯片,而不是一款芯片用在所有的场景里,那你就需要能够快速设计、快速验证、快速制造出货的人。RISC 在这方面的优势就改变了整个芯片生态的发展。

David:RISC 有很大优势。John 说的设计时间是一方面,能耗也是一方面。既然你用的晶体管更少了,芯片的能耗比也就更高了。

John:当你需要做低价的芯片,比如物联网领域的芯片的时候,你可能需要做每片只要 1 美元的处理器。X86 这样的有复杂翻译机制的芯片是没办法做到每片 1 美元的。

Diane:我想问问,现在苹果、谷歌都在做自己的芯片,以前他们都没这样做。现在发生什么了?

David:是的。一开始谷歌所有东西都是买现成的,现在慢慢地谷歌开始设计自己的计算机、自己的网络。John 以前也说过,这些以前都是扁平的企业,现在都开始做垂直整合了。

Diane:看到这样的现状你开心吗?

David:算是吧。如果你做的工作是卖新的点子,那你就希望能够找到很急切地希望尝试新点子的人。当市场上只有 Intel 和 ARM 两家指令集和芯片的承包商的时候,你必须去说服他们,写了论文以后要先去求他们。他们是守门的人。而谷歌这样的以软件为基础的企业就对硬件创新更开放一些,因为只要在他们自己的企业里面起效就可以了。

John:这样现状是因为哪里有创新的机会,哪里就会往前发展。之前很长的时间里我们都关注的是那些通用计算的芯片,它们变得越来越快。那么 随着通用芯片的效率变得越来越低,我们需要另辟蹊径。那么我们找到的方案就是为特定的任务优化芯片设计,比如为机器学习设计 TPU,它是专用的计算芯片。那么谁有设计专用芯片所需的知识呢?就是这些垂直整合的企业们,它们有软件设计的能力,可以专门为硬件设计语言和翻译系统。这里也是一个有趣的变化,我觉得以后做计算机体系设计的人要变得更像文艺复兴时期的人,他们要对整个技术堆栈都有了解,要能够和最上层的写软件的人沟通,用和现在完全不一样的方式了解他们要的是什么。这对整个领域都很有意思。

Diane:因为太专用了,设计流程仿佛都倒过来了。做云服务的人能看到服务器上都在进行什么样的运算,他们看到的可能反倒比做处理器的人看到的还要多、还要明白。

David:对。这也是另一点有趣的地方。对云服务提供商来说,他们是一个闭环的生态系统,在企业内部它只需要解决一个局部的问题,不需要考虑通用计算市场和各种奇怪的使用状况; 它只需要解决那一个环节的计算就可以了。所以这也会缩短开发时间。目前来看,这些大企业都很大胆地做出了各自的行动,微软在 FPGA 上有很多投入,谷歌在做自定义的芯片,传闻说 Amazon 等等也在做一些全新的东西。所以这个时代很有趣,可以看到很多创新。

Diane:腾讯和阿里巴巴的情况如何?

David:嗯,我觉得他们也在做芯片。

John:我觉得现在这个时候很有趣,是因为有一件我们没有预计到的事情发生了。虽然我们切换到了高级语言和标准的操作系统上来,但是 80 和 90 年代的时候你的硬件选择反倒变少了。PC 的市场占有率太高了,大家都不得不关注 PC,很多一开始专门给 Mac 写软件的企业都变成了专门给 PC 写软件的企业,就是因为 PC 几乎占据了整个空间,这限制了这个了领域可以出现的小创意和大创新。那么一旦我们有了很多的创新的量,我们就可以做出很多新的东西。

Diane:它对创新的限制就是因为是它单方面决定了整个过程,别人都要围绕着它来。

David:与 x86 指令集的二进制兼容性是一件非常有价值的事情。现在让我来看的话,这些垂直整合的企业都是在提升抽象的级别,比如从 x86 二级制指令到 TensorFlow,这是很大的一个跨越。但是到了那个抽象的高度以后,我们就有很多的空间创新、把事情做得更好。

Diane:那语言和框架呢?

David:如果抛开硬件架构不谈,现在有这种 让程序员变得更加有生产力的运动。如果你是刚入门的计算机科学家,你肯定会学 Python,这是一种很酷的语言,有很多很强大的功能,也有 Jupiter Notebooks 支持,所以它带来的生产力很高。整个世界都有这样的趋势,我们可以看到 Python 这样的脚本语言、TensorFlow 这样的特定领域专用的语言等等,它们服务的用户群都更窄,就是为了提高用它们的人的生产力。

John:我觉得这就是正确的方向。如果我们想要有很高的计算性能的同时还要保持软件生产力的话,你知道只是逼程序员们写更高效的程序、发挥更多硬件能力是不行的,硬件本身也要对任务有所优化。那么我们不仅需要对编程语言做创新,还需要对整个编程环境做创新,然后把运行的结果反馈给程序员们。

Diane:这样它就能不断自己改进了。到时候全世界的人、小学生都可以编程了

John:你想象一下, 三年级的小学生在用机器学习,简直是不可思议

Diane:你们认为最终大家都会用某一款芯片做机器学习吗?

David:以我们的职业经历而言,我觉得这是一批超乎寻常地快速发展的应用领域,由于摩尔定律失效了,它就很需要定制化的计算架构。你知道,典型的计算机架构设计就像是用 打飞盘,我们的子弹飞出去要花好几年,但是飞盘飞得太快了,等到子弹过去的时候谁知道飞盘已经飞到哪里了。那么我们现在看到有这么多企业都专门为任务设计优于标准微处理器的硬件,但是谁知道谁的点子最好呢,尤其是这个领域还在继续快速发展。据说现在有四五十家机器学习硬件初创公司,我们期待看到大家尝试各种各样不同的点子,然后看看最终谁能胜出。历史上都是这样,如果你回头看计算机架构的市场占有率,每个人都会做出自己的努力,然后逐渐有人胜出,有人退场了。

Diane:你觉得他们会不会受制于需要配合的那个软件?

David:这里的考量是,因为我们提高了编程所在的抽象级别,所以不会受到限制。不然就是经典编程的问题了。

John:世界还有一个重要的变化是,如果你回头看 80 年代、90 年代甚至是 2000 年左右的计算机,台式计算机和小型服务器还是绝对主流的计算模式。然后突然就是云计算、移动设备和 IoT了,不再是只有中间那一小块空间了。这就是说,对于能耗比、性价比的取舍,我们现在可以有许多种不同的选择。这边我可以造一个 1 美元的处理器用在 IoT 设备上,那边可以有一个水冷的三代谷歌云 TPU,许多不同的运行环境,许多不同的设计考量。它也就提供了很高的灵活程度。

David:我现在觉得,这中间是什么呢,中间的设备需要考虑二进制兼容性。在云服务器上二进制兼容性不重要,在大多数 IoT 设备上二进制兼容性也不重要。我们只需要创新就好了。

Diane:嗯,这些限制都不见了,那很棒。未来即将要到来的是量子计算,跟我们讲讲这是什么、讲讲你们的看法吧。

John:量子计算是「 未来的技术」,唯一的问题是它「永远都会是未来的技术」还是有一天会真的到来。这个问题挺开放的,我自己的思考角度是, 如果大多数研究量子计算的都是物理学家,而不是工程师的话,那离我们就还有至少 10 年时间那么现在做量子计算的多数都是物理学家

量子计算的真正难度在于能否拓展它的规模。对于某一些问题它有着无可比拟的优势,其中一个它能解决得非常好的问题是因数分解,这其实对我们现在的信息安全提出了挑战,因为 RSA 算法的关键点就在于难以做大数的因数分解;这会给我们带来一些麻烦。其它很多方面也有优势,比如量子化学可以给出非常精确的化学反应模拟结果。但是如果要做大规模有机分子的模拟之类的真正有趣的事情,你就需要造一台很大的量子计算机。大家不要忘了,量子计算机的运行温度只比绝对零度高几 K,那么我实际上就需要保持量子计算机的运行环境非常接近绝对零度。这件事很难做。而且,室内的振动、数据的采集甚至如果量子计算机没有做好电磁防护而你带着手机走进了屋里,量子计算机的状态都会完全改变。为了让它能够计算,就要保持它的量子状态,然后最终再采集它的量子状态。这其中的物理规律很惊人,我们肯定能够在研究中学到很多这个领域的知识,但是未来的通用量子计算机会怎么发展,这个问题就很开放了。

David:我觉得量子计算机和核聚变反应堆差不多,都是非常好的研究课题。如果真的能让它们工作起来的话,对整个世界都是很大的推动作用。但是它离我们起码还有十几年的时间,我们的手机也永远都不会是量子计算的。所以,我挺高兴有这么多人在研究它,我也很敬仰愿意做这种长期研究的人,你知道,以我自己来说,我的职业生涯里很难预测 5 年或者 7 年之后的事情,所以我做的事情都是关注短期一些的目标,比如花 5 年做一个项目,然后希望再过几年它可以改变世界。不过我们也经常会错。你预测的东西离现在越远,想要预测对就越难。

Diane:你们两位都是在学术研究的环境里成长,然后加入了企业。不过学校和企业之间的关系也在不断变化吧,你们是怎么看的?

David:计算机领域有一个很大的优点是 学术界和业界之间的关系是协同的、互相促进。其他一些领域,比如生物学,他们是对抗性的关系,如果你在学术界你就只能做研究,到了企业就只能卖东西。我们这边不是这样的。

Diane:现在也没问题吗?现在大公司不是把学校里的教授全都招走了?

John:这确实是个问题,如果做机器学习的人全都跑到业界去了,就没人来教育以后新一辈的机器学习人才了。

David:过去的 5 年里人们对于机器学习的兴趣一下子爆发了,机器学习也确实有不小的商业意义,所以做机器学习的人的薪水也在上升,这确实有点让人担心。我们两个人职业生涯早期也遇到过类似的情况,也是一样的 怕把种子当粮食吃了。当时微处理器以及别的一些东西因为太赚钱了,就会把所有大学里的人都吸走,没有人教育未来的人才了。现在机器学习确实有这方面的问题,不过你从全局来看的话,总是源源不断地有聪明的年轻人想要研究学术,所以也不会 100% 的全都离开学校的。John 还做过校长呢,你也说说。

John:像你刚才说的,我们这个领域的一大优点就是学术界和业界的互哺,企业的人和学校的人做的事情虽然不同但是也有项目的尊重。有许多领域都不是这样的,学术界的人觉得业界的人做的是 无聊的工作,业界的人觉得学术界的人做的是 没用的工作。计算机科学领域就不是这样的。其中一个原因可能是因为这个领域一直都发展很快、有很多事情在发生。你做的某项科研成果,10 年后就可能改变这个领域。这真的很棒。

Diane:我可不可以这么讲,计算机领域的长期研究主要是在学术界,短期研究主要是在企业?

David:差不多吧。

John:对,差不多吧。不过当然也有一些企业在做长期的投资,比如谷歌收购 DeepMind 就是一项长期的投资。微软和谷歌也都在量子计算方面有很多投资,这些都是长期的投资。这和当年 AT&T 收购了贝尔实验室的感觉差不多,都是长期的投资,而且这些投资让整个国家受益匪浅。

Diane:工程技术也随着科学技术在发展。最近我听说亚利桑那州有个人,Michael Crowe,创办了一所工程学校。你们怎么看?

John:人们当然是在对工程本身做一些创新。计算机科学相比别的学科在工程方面也有很大的优势。我们有很多跨学科的内容,可以说有很多跨学科带着计算机科学向前走,这种趋势非常明显。有一些学科始终都起到核心的作用,比如医学和一些社会科学,那么大数据、机器学习的革命来临之后,社会科学发生了革命,我们对人类自己的了解、对整个社会的了解、如何改进整个社会都有了新的发现,这都很重要。

那么计算机科学呢,当我 2000 年当上斯坦福大学校长的时候,我觉得计算机科学已经发展到头了,它就那样了。然后学生物的、学医学的人开始说「二十一世纪是生物学的世纪」, 开始搞功能基因组学之类的东西 —— 我不是说功能基因组学不重要,不过计算机科学可能是功能基因组学里最重要的东西,有计算机科学才能做出其中的那些发现。

所以我们看到了这一场难以置信的革命,我们看到学生的数目开始增长,以及谢天谢地,这么多年了,终于看到女学生开始变多一点了。这都是非常健康的现象。我们在吸引着他们,全国的、全世界的最聪明最优秀的人才都加入了这个领域,这让我非常激动。这也改变了整个世界。

David:当我和 John 刚加入这个领域的时候,其实我们自己的亲戚都觉得我们是不是入错行了,「行吧你想做就做吧」,就这样。

John:我爸都说,「做硬件还行,千万别做软件」。(笑)

Diane:我们看到科技行业吸引了这么多的资金,你们自己的学生创办了好多企业,John 也建立过自己的企业等等。比尔盖茨现在不做了,全职做慈善。你们做老师的时候也像是慈善事业。那么你们怎么看慈善的事情,以及整个科技行业里的人。

David:我觉得,当年我拿到 UC 伯克利的 Offer 之后,过了一阵子才去报道。当时我看了一本书,名叫《Working》,里面采访了四十个不同行业的人,让他们谈谈自己的职业。我从书里读到的是,你要么要做一些结果很持久的事情,比如造帝国大厦,或者造金门大桥,要么和别人一起合作,比如做教师或者做牧师。这样的事情能带给你满足感,因为它们能影响到别人的生活。我自己就比较期待这样的工作。其实在美国,大家默认认为等你有钱了你就开心了,但是其实如果你的目标是开心的话,你就直接向着开心去就好了,挣钱在其中不一定有多么重要。我几十年工作的驱动力就差不多是这样的。有的人其实做了研究,研究什么东西会让人快乐,其中一件事就是帮助别人。 影响别人、帮助别人能让你感到开心。所以我觉得如果你想要变得开心,你就应该帮助别人。

John:讨论这个还挺有趣的。我记得我大概 25 年前和比尔盖茨有过一次讨论,我问他对慈善的观点是怎么样的。他说,微软的事情太多太忙了,我现在还没有时间考虑这个。不过如果你见过比尔盖茨本人的话,你就知道他是一个非常专注、非常自我驱动的人,从他管理微软的方式上你也能看得出来。后来当他做慈善的时候,那真的是完完全全的慈善家,他可以和斯坦福医学院的人坐下来谈生物学和疾病感染,谈得非常的深入。他和妻子梅琳达是真的非常投入地要让这个世界变得更好。Gordon Moore 也是这样,他建立了摩尔基金会,在科学和保护区两件大事上花了很多钱。

比尔盖茨做慈善的时候很开心,他真的很喜欢慈善事业,他和梅琳达也是很棒的搭档。我在阿拉斯加看到了 Gordon Moore 做的濒危野生鲑鱼的栖息地保护区,和 Mark Zuckerberg 和他妻子 Priscilla 讨论他们的慈善想法,讨论如何减轻人类疾病的影响,都非常棒。我觉得其中每一个例子、每一件事,都给他们的生命带来了一些很激动有趣的东西。

之前我做斯坦福大学校长的时候,我经常在想有什么办法激励别人变得更慈善一些。然后我看了《Alexander Hamilton》的作者 Ron Chernow 写的另一本书,讲石油大亨洛克菲勒的事,他快 50 岁的时候得了心脏病,差点死掉,然后他就退休了,这辈子剩下的时间都在做慈善,他创办了芝加哥大学,他建立了洛克菲勒基金会,一直好好活到快 100 岁,非常美满。所以我觉得回报他人能带来快乐,我们都是聪明的、有创意的人,都能把事情做好。这也是能真正地让世界变得更好的事情。

(完)

雷锋网 AI 科技评论整理编译。

每秒百万级流式日志处理架构的开发运维调优笔记 | Cloud

$
0
0

荣幸之至,我们团队在实时日志分析、搜索项目中曾经应对过百万级的挑战,在这方面有长足的进步。本文以笔记和问答的形式记录了我们曾经遇到过的实际问题及解决方案,而非小白式的大数据科普文章。相信只有真正做过每秒近百万以上的实时日志处理业务,遇到过棘手问题,才能深刻感受我们当时越不过高坎的窘境与解决问题后的狂喜。

架构图及架构介绍

我们首先对架构做一个简单的介绍,在产生日志的服务器上通过agent(rsyslog或其它工具)把日志发送到数据平台的接收接口(nginx或flume等架设的http或tcp接口),通过kafka队列,经过Spark的ETL输出统一格式的日志到Kafka,进而使用Elasticsearch索引全量数据提供全文搜索服务,同时数据存储到我们内部的云图系统提供海量统计分析服务。最终用户打开我们定制开发的UI,即点即用,完成业务分析需求。

架构不复杂,但一旦涉及海量和实时,必定充满挑战。我们对数据平台做了无数次优化,实则在不停得迭代回答以下几个问题:

  • 如何做到日志源将日志尽可能均匀打到日志接收节点,实现负载均衡?

  • 如何做到不丢日志?

  • 如何做到处理能力每秒近百万?

  • 如何降低搜索和统计的延迟?

  • 如何用更少的成本实现以上目标?

我们计划通过一系列的文章介绍我们的思考与解决方案,本文后面的内容是系列的开篇,是架构中主要开源技术的开发、运维、调优的笔记。

Rsyslog

  • 问题1: 通配日志文件名?从最新位置开始读?
    v8.15.0 之前不支持imfile 通配,不支持从最新位置开始读; centos 6.x 上默认安装的是 v5.8.10

  • 问题2: rsyslog 直接写kafka ?
    v8.7.0 之前不支持 omkafka, 不能直接写kafka; omkafka 底层使用librdkafka库(kafka c api client), 发送队列满后会丢弃数据,将导致丢数据。

  • 问题3: 不会配置rsyslog ?
    v5.8.10 配置模版(从文件读日志发送到flume):
    http://git.letv.cn/oi/configs/blob/master/log_agent/rsyslog/oi_rsyslog_agent.conf.template

  • 问题4: 使用omfwd tcp 发送数据时,如何实现 rsyslog 以load balance的方式向多个接收端发送日志?

如果发送端 rsyslog节点个数较多(发送端个数数量级大于接收端),可以在发送端与接收端之间部署LVS或haproxy 等 load balance工具。

如果发送端 rsyslog节点个数很少,如 rsyslog 发送端有4个,接收端有5个,由于 rsyslog 与接收端建立长链接,将导致多个发送端将数据发送到同一个接收端上。此时常见的有2种方案:
(1)在有 lvs 或 haproxy 的基础上,配置omfwd的 rebindinterval 参数,每次发送一定量的数据后与接收端重新建立链接。代价是此方案不是并行的向多个接收端发送数据,还需要关注一下lvs/haproxy的负载均衡策略,防止单台接收端上建立过多链接。rsyslog v7.x之前,这个参数的名称是:\$ActionSendTCPRebindIntervalinteger(或者 \$ActionSendUDPRebindIntervalinteger。

(2)用mmsequence生成随机数字,然后按照这个数字来走不同的omfwd。好处是rsyslog可以并行得向多个接收端发送数据。代价是需要在 rsyslog 配置里写很长的action列表,配置有些繁琐。

在此感谢rsyslog大神@argv提供rsyslog负载均衡的思路。

Flume (v1.6.0)

  • flume 原理及核心概念:

source(syslogtcp/avro) -> channel(mem) -> sink(kafka/avro)

transaction[begin, commit], putlist, takelist

transactionCapacity, batch_size 的意义以及两者关系

  • flume 原理:MemoryChannel的设计

source:开启事务 -> 存数据 -> 放入私有的putlist->提交事务(消息进入channel,清空事务的takelist,putlist)
sink:开启事务 -> 从channel取数据 -> 放入私有的takelist->提交事务(消息进入channel,清空事务的takelist,putlist)

注:上图非原创,原图地址: http://www.cnblogs.com/dongqingswt/p/5070776.html

  • flume 原理:AvroSource,AvroSink事务

AvroSink在消费的事务中通过rpc调用对应的AvroSource的appendBatch方法,开启并提交了一次AvroSource的put事务,
AvroSource的生产拥堵(就是avro source 往后面的channel put 太慢,可能是source里配置了几个非常消耗CPU资源和时间的interceptor造成的)也会减慢对应的AvroSink的消费速度
AbstractRpcSink

AbstractRpcSink 主要源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int i = 0; i < client.getBatchSize(); i++) {
Event event = channel.take();
if (event == null) {
break;
}
batch.add(event);
}
...
client.appendBatch(batch);
...
transaction.commit();
  • flume 原理:SyslogTcpSource, KafkaSink事务

不同类型的source事务的提交机制不同,例如:kafka source是通过batch size和batch duration共同控制,kafka sink只通过batch size控制,而SyslogTcpSource是每写入一条消息就会开启并提交一次事务

SyslogTcpSource 主要源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (buff.readable()) {
Event e = syslogUtils.extractEvent(buff);
if (e == null) {
logger.debug("Parsed partial event, event will be generated when " +
"rest of the event is received.");
continue;
}
try {
getChannelProcessor().processEvent(e);
counterGroup.incrementAndGet("events.success");
} catch (ChannelException ex) {
counterGroup.incrementAndGet("events.dropped");
logger.error("Error writting to channel, event dropped", ex);
}
}

KafkaSink 主要源码:

1
2
3
4
5
6
7
8
if (processedEvents > 0) {
long startTime = System.nanoTime();
producer.send(messageList);
long endTime = System.nanoTime();
counter.addToKafkaEventSendTimer((endTime-startTime)/(1000*1000));
counter.addToEventDrainSuccessCount(Long.valueOf(messageList.size()));
}
transaction.commit();
  • flume 原理:flume1.6和1.7的KafkaSink batchsize差别

在flume1.7中kafka sink会在一个事务提交前强制发送kafka proudcer中缓存的消息,意味着值大于sink batch size(默认为100)的kafka.producer.batchsize将不生效

1
2
3
4
5
6
7
8
9
//1.7
for (; processedEvents < batchSize; processedEvents += 1) {
event = channel.take();
...
//producer.send
...
}
producer.flush();

理解了上面的原理后,我们来提出下面几个问题:

  • 问题1:重启flume会导致数据丢失吗?

使用memory channel时,重启将丢失channel中未处理完的数据。

  • 问题2:source 向 channel put 数据非常慢?

检查是否在source中配置了interceptors, 某些interceptor性能很差,如 regex_extractor。

  • 问题3:kafka sink 写入 kafka 0.9.0.1 时总是有异常错误?

flume 1.6.0 的 kafka sink 的 kafka api client 不兼容新版本的kafka, 需要自己编译 https://github.com/apache/flumetrunk 分支的最新版 kafka sink , 引入 flume 1.6.0 版本中。

  • 问题4: 如何定位 flume 性能问题?

以双层 flume 架构为例,数据流是:
rsyslog ===>
Flume_01 (syslogtcp source -> memory channel -> avro sink ) ===>
Flume_02 (avro source -> memory channel -> kafka sink) ===> Kafka

举个例子,如果第一层syslogtcp source 的tcp队列recv queue阻塞,但是后面的 channel size增长很慢,说明是syslogtcp source接收和处理太慢;如果第一层flume的channel size持续增长,说明Avro sink比较慢;如果第二层flume的channel size持续增长,说明kafka sink太慢。

这里有一个普适性规律:

(a) 如果是source慢,可以考虑去掉source后面的interceptor;如果是往channel put太慢,可以考虑增加batch size,channel transaction capicity size。

(b) 如果是sink慢,可以考虑增加sink的个数,增加并发度;增加sink发送的batch size;对于avro sink -> avro source 这样sink 通过rpc调用下一级source appendBatch() 的数据流,应该保证下一级source不阻塞。

  • 问题5: 如何提高 flume 性能?

(a) 尽可能使数据来源均匀打量到后段多个flume上

(b) 增加 flume 实例数

(c) 单 flume 实例增加 sink 个数 (sink 个数与线程个数相关)

(d) 少用 regex_extractor 等消耗性能的interceptor

  • 问题6: 如何通过直接配置文件路径实现从指定文件路径读取数据?

把flume 1.7.0 引入的 TaildirSource 加入到 flume 1.6.0 中

参考: http://lxw1234.com/archives/2015/10/524.htm

  • 问题7: 我们对Flume 1.6.0 做了哪些改动?

我们下载了flume 1.7.0 (还未正式发布)的源码,编译后,用里面的一些jar 包替换了flume 1.6.0 的 jar包,包括:kafka-sink , taildir-source; 并且增加了一些常用的interceptor, 如:。

我们在互联网上找到的一些不错的 flume 插件在这里列出:
https://github.com/garyelephant/awesome-flume-plugins

  • 问题8:如何开发自定义的inteceptor ?

参考static inteceptor 作为样板开发:
https://github.com/apache/flume/blob/trunk/flume-ng-core/src/main/java/org/apache/flume/interceptor/StaticInterceptor.java

下面还有几篇介绍自定义inteceptor 的文章:
https://medium.com/@bkvarda/building-a-custom-flume-interceptor-8c7a55070038
https://thisdataguy.com/2014/02/07/how-to-build-a-full-flume-interceptor-by-a-non-java-developer/
https://hadoopi.wordpress.com/2014/06/11/flume-getting-started-with-interceptors/

  • 问题9:如何用同一套 Flume 架构实现多用户、多种日志的高性能收集?

需求:多用户多种日志收集,高性能
rsyslog/avro source -> file channel -> kafka sink

我们的解决方案:
(1)apptag interceptor
(2)修复 kafka sink 向未创建 topic produce message时的严重性能bug

1
2
org.apache.kafka.clients.NetworkClient$DefaultMetadataUpdater.handleResponse:582) - Error while fetching metadata with correlation id 5077 : {CROND[62459]:=UNKNOWN_TOPIC_OR_PARTITION, CROND[62458]:=UNKNOWN_TOPIC_OR_PARTITION, CROND[62460]:=UNKNOWN_TOPIC_OR_PARTITION}
23 Sep 2016 18:32:52,450 WARN [kafka-producer-network-thread | producer-5] (

Kafka (v0.9.0.1)

  • 问题1: 配置没有生效?无法produce, consume数据?

可能是 kafka api 版本问题
参见streamingetl的代码和配置。

  • 问题2:如何不丢数据 ?

producer端:

acks = “all”
retries = “2147483647” (Long.MAX_VALUE)
= Long.MAX_VALUE

consumer端:
Disable auto.offset.commit
Commit offsets only after the messages are processed:Spark Direct Kafka Stream, 自己控制offset 的commit

topic设置:

replication factor >= 3

至少出现一次 -> 数据可能重复

  • 问题3:如何提高producer性能 ?

增大 linger.ms, 默认值: 0
增大 batch.size, 默认值:16384
max.in.flight.requests.per.connection = 5
compression.type = snappy,默认不压缩

多 producer 实例, 提高并发

  • 问题4:如何提高consumer(old consumer)性能 ?

kafka 0.9.0.0 引入的new consumer 还处于测试阶段,暂不考虑。

num.consumer.fetchers = 1
fetch.message.max.bytes = 2 10241024

多个 consumer 实例

  • 问题5:kafka 性能问题一般出现在哪里?

kafka 性能一般受限于 kafka broker 的带宽的大小,CPU, Disk IO 极少成为瓶颈;使用 snappy 等压缩方式压缩message有助于减少broker 带宽压力,一般能压缩到原来的5/6。

  • 问题6: 如果一个broker节点挂了,我需要做什么?/ 如何做到不丢数据的情况下扩容,缩容,更新broker的配置?

Kafka 不能像 Elasticsearch 一样能自动做数据的 rebalance 和 replicas recovery。如果某个borker 异常下线,将导致部分topic的partition副本数不够;扩容节点时,新加入的节点上不会自动分配topic 的parition,处于空闲状态。综上,在扩容,节点异常下线后,缩容前,都应该做数据的rebalance。可以参考官方的具体操作 Expanding Your Cluster, 更方便的是直接使用 Kafka Manager中的”Generate Partition Assignments”,”Reassign Partitions”功能。

Spark

  • 问题1: 使用 python + spark 做实时日志分析会遇到什么困难 ?

(a) pyspark executor 进程模型(jvm fork出 python进程):根据partiton数量生成python 多个进程

(b). virtual memory exceeded error : fork 出了很多 python进程,每个python 进程占用 400M + virtual memory

(c) kafka-python package 向 kafka 0.9 写数据存在未知bug 导致 kafka 集群不可用(invalid message size)

(d) 处理同样量级的数据,需要更多资源(导致某些节点负载过高,卡住这些节点上的其它spark job)

(e) task processing time 极度不均衡导致处理能力下降(单个task processing time 远大于平均)

(f) 无法实现广播变量(spark 官方doc的例子可能是假的)

结论:大规模 spark streaming,输出是 kafka 的不要使用 python 作为开发语言。

  • 问题2: 写数据到 kafka 频繁建断链接?

用广播变量(broadcast variable),单个executor (同一个jvm 程序)的不同task 可以复用同一个kafka producer。

  • 问题3:如何提高 spark streaming 任务处理并行度?

影响 spark 程序并行度的两个重要参数:(1) job partition 个数 (2) concurrent job(spark.streaming.concurrentJobs) 个数。常见的是通过repartition可以调节partition个数,要做好repartition耗时与并行度之间的平衡。默认情况下concurrentJobs=1, 一个batch只能执行一个job。concurrentJobs=2时,如果下一个batch已经开始,上一个batch还没执行完,就会出现2个active job并行执行。在这种情况下,只要一个batch的job的执行时间不超过2个batch的duration,就不会出现batch scheduling delay。即,假设 concurrentJobs=n, 一个job的实际执行时间为t, 一个batch的duration设置的是d, 只要 t < n * d 就不会出现scheduling delay。

例如,对于kafka direct stream,kafka topic partition 个数在实现上是映射为相同的 spark job partition个数, 可以通过增加数据来源kafka topic partition 个数来增加spark job的 partition个数.

  • 问题4:如何不丢数据?

spark streaming的stopGracefully策略允许处理完receiver已收到但还未处理的数据后,再停止application。

spark streaming的checkpoint 可以做到至少处理一次,但会对spark streaming有一定的性能损耗。

kafkaDirectStream 可以实现先处理完数据,再更新offset,也可以保证至少处理一次,在这种场景下不需要spark streaming 的 checkpoint 做保证。

  • 问题5: kafkaDirectStream和kafkaS1tream对比 ?

采用kafkaStream的情况下如果接收的数据没处理完会出现重启spark job丢失数据的情况,结合stopGracefully和checkpoint机制才能保证数据不丢失

采用kafkaDirectStream需要自己维护offset,优点是不需要stopGracefully和checkpoint就能保证数据不丢失

  • 问题6: spark programming 常见优化技巧?

(a) mapPartition vs map

http://lxw1234.com/archives/2015/07/348.htm
https://martin.atlassian.net/wiki/pages/viewpage.action?pageId=67043332

  • 问题7: 为什么我的spark job 处理慢?

进入spark 监控界面,查看Jobs里的各个stages,确认是普遍都慢还是个别stages耗时较长;
同时确认是每个executor上的task耗时长,还是个别executor上执行的task耗时长。如果是个别executor的task执行时间较长,有可能是这个executor所在的节点CPU耗尽或负载较大。

如下图,有2个executor,其中一个执行task耗时明显比另外一个时间长

如果在executors监控页面中也看到某个executor的task time明显长于其它executor,说明在此executor上,可能有很多stages的task 执行时间都过长。

Elasticsearch

  • 问题1: 如何加快 recovery 速度?

  • 问题2: 集群 recovery 时数据不能写入?

_cat/pending_tasksapi 查看es待执行的任务看到,因为recovery task 的 priority 是urgent, 所以会阻塞其它task,如:create index 时的 put mapping task(priority 是 high), put mapping 失败,导致对应index的数据写入失败。

1
2
585362 1h URGENT shard-started ([myindex-2016.09.19.13][81], node[4dhqt3jkRkuGh7QGtWNSnw], [R], v[4], s[INITIALIZING], a[id=eyyKbVfkSxGWrjJPvkTkNA], unassigned_info[[reason=REPLICA_ADDED], at[2016-09-23T03:19:15.479Z]], expected_shard_size[6557737229]), reason [after recovery (replica) from node [{10.148.67.19}{e01bOwmGQsuRmnkqUIad-Q}{10.148.67.19}{10.148.67.19:9300}{max_local_storage_nodes=1, master=false}]]
585363 1h URGENT shard-started ([myindex-2016.09.19.13][90], node[LXXvJpOUSs2X2Pzvx-1hWg], [R], v[4], s[INITIALIZING], a[id=f4hpdO7NTd-u4fLLSjQBHw], unassigned_info[[reason=REPLICA_ADDED], at[2016-09-23T03:19:15.479Z]], expected_shard_size[4717770969]), reason [after recovery (replica) from node [{10.148.67.20}{X10U43ibR5aDx7TzKVGQZw}{10.148.67.20}{10.148.67.20:9300}{max_local_storage_nodes=1, master=false}]]

你可以在es日志中看到大量的如下put mapping 失败的信息:

1
2
3
4
5
[2016-09-23 13:36:24,394][DEBUG][action.admin.indices.mapping.put] [10.148.67.11] failed to put mappings on indices [[myindex-2016.09.23.04]], type [logs]
ProcessClusterEventTimeoutException[failed to process cluster event (put-mapping [logs]) within 30s]
at org.elasticsearch.cluster.service.InternalClusterService$2$1.run(InternalClusterService.java:349)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

对 es 执行 task 的优先级定义、执行顺序、并行个数感到好奇!recovery 的时候,观察到集群负载并不高,但是 index/search 很慢。

另外还有2个人在elasticsearch google group 上 询问recovery/index/search priority的,但是没有人回复:
https://groups.google.com/d/msg/elasticsearch/2XQuTnrtuVI/xQ7CuL0x5HkJ
https://groups.google.com/d/msg/elasticsearch/bEX1XngiIXg/p5YDLWnVrJEJ

  • 问题3: hangout 遇到 es recovery时的意外停止写入数据?

hangout, flume 没有反压机制,sink发不出去的时候,source会继续接收数据,直到channel满,OOM。

  • 问题4: 为什么ES节点的 Heap Usgae 总是很高?

(a) translog 设置的太大, index settings 里面:index.translog.flush_threshold_size: “200mb”的意思是index的每个shard最大translog buffer size,可能到达这个size后才flush,如果这个值设置的很大(我们以前误以为这个值越大越好,所以设置为10g),在flush会占用过多内存。

(b) indexing buffer 设置的太大.

(c) 其它原因待整理.

常见性能问题定位方法及工具

  • 查看618端口 tcp recv queue 最大的socket链接

ss -n | grep 618 | sort -k2 -n -r | head

  • 查看 pid:13323 进程正在进行的系统调用

strace -f -p 13323

  • 统计 pid:13323 进程系统调用计数

strace -f -c -p 13323

  • 查看 pid:13323 的进程打开的文件描述符

lsof -p 13323

  • 查看是哪个进程开启的618端口

lsof -i:618

  • 查看历史负载/查看历史网络流量

sar -q / sar -n DEV

  • 每隔 5s 统计一次磁盘IO

iostat -dtx 5

  • 基于 jvm 的程序如何定位 ?

大部分大数据相关的开源软件如hadoop, spark, kafka, flume 等都是基于JVM的程序,它们都可以通过配置开放JMX端口,查看到 jvm 程序的详细运行时信息(内存,GC,线程,对象…)。

总结:

  • 开源软件很优秀,也有诸多性能的问题和bug。

  • 解决性能问题不能只靠猜和试 - 尝试做好监控和阅读源码。

  • 性能调优分几个层级:硬件级别,OS 级别,程序代码级别,架构级别;请认准特定环境下投入产出比最大的层级进行调优。

  • 系统资源瓶颈就是4种资源的瓶颈: CPU (占用率), Memory(占用率), Disk(剩余空间, IO速度, IO利用率), Network(网络RTT时间,网卡速率上限)。


本文由乐视云计算OI数据平台 @高英举,@王捷共同撰写。


References:

  1. “lsof command examples” http://www.thegeekstuff.com/2012/08/lsof-command-examples/

Elasticsearch 调优实践

$
0
0



  背景

Elasticsearch(ES)作为NOSQL+搜索引擎的有机结合体,不仅有近实时的查询能力,还具有强大的聚合分析能力。因此在全文检索、日志分析、监控系统、数据分析等领域ES均有广泛应用。而完整的Elastic Stack体系(Elasticsearch、Logstash、Kibana、Beats),更是提供了数据采集、清洗、存储、可视化的整套解决方案。 

本文基于ES 5.6.4,从性能和稳定性两方面,从linux参数调优、ES节点配置和ES使用方式三个角度入手,介绍ES调优的基本方案。当然,ES的调优绝不能一概而论,需要根据实际业务场景做适当的取舍和调整,文中的疏漏之处也随时欢迎批评指正。



性能调优

一 Linux参数调优


1. 关闭交换分区,防止内存置换降低性能。 将/etc/fstab 文件中包含swap的行注释掉

  1. sed-i'/swap/s/^/#/'/etc/fstab

  2. swapoff-a


2. 磁盘挂载选项

  • noatime:禁止记录访问时间戳,提高文件系统读写性能

  • data=writeback: 不记录data journal,提高文件系统写入性能

  • barrier=0:barrier保证journal先于data刷到磁盘,上面关闭了journal,这里的barrier也就没必要开启了

  • nobh:关闭buffer_head,防止内核打断大块数据的IO操作

  1. mount-o noatime,data=writeback,barrier=0,nobh/dev/sda/es_data


3. 对于SSD磁盘,采用电梯调度算法,因为SSD提供了更智能的请求调度算法,不需要内核去做多余的调整 (仅供参考)

  1. echo noop>/sys/block/sda/queue/scheduler


二 ES节点配置


conf/elasticsearch.yml文件:

 1. 适当增大写入buffer和bulk队列长度,提高写入性能和稳定性


  1. indices.memory.index_buffer_size:15%

  2. thread_pool.bulk.queue_size:1024


2. 计算disk使用量时,不考虑正在搬迁的shard

在规模比较大的集群中,可以防止新建shard时扫描所有shard的元数据,提升shard分配速度。

  1. cluster.routing.allocation.disk.include_relocations:false


三 ES使用方式


1. 控制字段的存储选项

ES底层使用Lucene存储数据,主要包括行存(StoreFiled)、列存(DocValues)和倒排索引(InvertIndex)三部分。 大多数使用场景中,没有必要同时存储这三个部分,可以通过下面的参数来做适当调整:

  • StoreFiled: 行存,其中占比最大的是source字段,它控制doc原始数据的存储。在写入数据时,ES把doc原始数据的整个json结构体当做一个string,存储为source字段。查询时,可以通过source字段拿到当初写入时的整个json结构体。 所以,如果没有取出整个原始json结构体的需求,可以通过下面的命令,在mapping中关闭source字段或者只在source中存储部分字段,数据查询时仍可通过ES的docvaluefields获取所有字段的值。

    注意:关闭source后, update, updatebyquery, reindex等接口将无法正常使用,所以有update等需求的index不能关闭source。

  1. # 关闭 _source

  2. PUT my_index

  3. {

  4.  "mappings":{

  5.    "my_type":{

  6.      "_source":{

  7.        "enabled":false

  8.      }

  9.    }

  10.  }

  11. }

  12. # _source只存储部分字段,通过includes指定要存储的字段或者通过excludes滤除不需要的字段

  13. PUT my_index

  14. {

  15.  "mappings":{

  16.    "_doc":{

  17.      "_source":{

  18.        "includes":[

  19.          "*.count",

  20.          "meta.*"

  21.        ],

  22.        "excludes":[

  23.          "meta.description",

  24.          "meta.other.*"

  25.        ]

  26.      }

  27.    }

  28.  }

  29. }

  • docvalues:控制列存。

    ES主要使用列存来支持sorting, aggregations和scripts功能,对于没有上述需求的字段,可以通过下面的命令关闭docvalues,降低存储成本。

  1. PUT my_index

  2. {

  3.  "mappings":{

  4.    "my_type":{

  5.      "properties":{

  6.        "session_id":{

  7.          "type":"keyword",

  8.          "doc_values":false

  9.        }

  10.      }

  11.    }

  12.  }

  13. }

  • index:控制倒排索引。

    ES默认对于所有字段都开启了倒排索引,用于查询。对于没有查询需求的字段,可以通过下面的命令关闭倒排索引。

  1. PUT my_index

  2. {

  3.  "mappings":{

  4.    "my_type":{

  5.      "properties":{

  6.        "session_id":{

  7.          "type":"keyword",

  8.          "index":false

  9.        }

  10.      }

  11.    }

  12.  }

  13. }

  • all:ES的一个特殊的字段,ES把用户写入json的所有字段值拼接成一个字符串后,做分词,然后保存倒排索引,用于支持整个json的全文检索。

    这种需求适用的场景较少,可以通过下面的命令将all字段关闭,节约存储成本和cpu开销。(ES 6.0+以上的版本不再支持_all字段,不需要设置)

  1. PUT/my_index

  2. {

  3.  "mapping":{

  4.    "my_type":{

  5.      "_all":{

  6.        "enabled":false 

  7.      }

  8.    }

  9.  }

  10. }

  • fieldnames:该字段用于exists查询,来确认某个doc里面有无一个字段存在。若没有这种需求,可以将其关闭。

  1. PUT/my_index

  2. {

  3.  "mapping":{

  4.    "my_type":{

  5.      "_field_names":{

  6.        "enabled":false 

  7.      }

  8.    }

  9.  }

  10. }


2. 开启最佳压缩

对于打开了上述_source字段的index,可以通过下面的命令来把lucene适用的压缩算法替换成 DEFLATE,提高数据压缩率。

  1. PUT/my_index/_settings

  2. {

  3.    "index.codec":"best_compression"

  4. }


3. bulk批量写入

写入数据时尽量使用下面的bulk接口批量写入,提高写入效率。每个bulk请求的doc数量设定区间推荐为1k~1w,具体可根据业务场景选取一个适当的数量。

  1. POST _bulk

  2. {"index":{"_index":"test","_type":"type1"}}

  3. {"field1":"value1"}

  4. {"index":{"_index":"test","_type":"type1"}}

  5. {"field1":"value2"}


4. 调整translog同步策略

默认情况下,translog的持久化策略是,对于每个写入请求都做一次flush,刷新translog数据到磁盘上。这种频繁的磁盘IO操作是严重影响写入性能的,如果可以接受一定概率的数据丢失(这种硬件故障的概率很小),可以通过下面的命令调整 translog 持久化策略为异步周期性执行,并适当调整translog的刷盘周期。

  1. PUT my_index

  2. {

  3.  "settings":{

  4.    "index":{

  5.      "translog":{

  6.        "sync_interval":"5s",

  7.        "durability":"async"

  8.      }

  9.    }

  10.  }

  11. }


5. 调整refresh_interval

写入Lucene的数据,并不是实时可搜索的,ES必须通过refresh的过程把内存中的数据转换成Lucene的完整segment后,才可以被搜索。默认情况下,ES每一秒会refresh一次,产生一个新的segment,这样会导致产生的segment较多,从而segment merge较为频繁,系统开销较大。如果对数据的实时可见性要求较低,可以通过下面的命令提高refresh的时间间隔,降低系统开销。

  1. PUT my_index

  2. {

  3.  "settings":{

  4.    "index":{

  5.        "refresh_interval":"30s"

  6.    }

  7.  }

  8. }


6. merge并发控制

ES的一个index由多个shard组成,而一个shard其实就是一个Lucene的index,它又由多个segment组成,且Lucene会不断地把一些小的segment合并成一个大的segment,这个过程被称为merge。默认值是Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),当节点配置的cpu核数较高时,merge占用的资源可能会偏高,影响集群的性能,可以通过下面的命令调整某个index的merge过程的并发度:

  1. PUT/my_index/_settings

  2. {

  3.    "index.merge.scheduler.max_thread_count":2

  4. }


7. 写入数据不指定_id,让ES自动产生

当用户显示指定id写入数据时,ES会先发起查询来确定index中是否已经有相同id的doc存在,若有则先删除原有doc再写入新doc。这样每次写入时,ES都会耗费一定的资源做查询。如果用户写入数据时不指定doc,ES则通过内部算法产生一个随机的id,并且保证id的唯一性,这样就可以跳过前面查询id的步骤,提高写入效率。 所以,在不需要通过id字段去重、update的使用场景中,写入不指定id可以提升写入速率。基础架构部数据库团队的测试结果显示,无id的数据写入性能可能比有_id的高出近一倍,实际损耗和具体测试场景相关。

  1. # 写入时指定_id

  2. POST _bulk

  3. {"index":{"_index":"test","_type":"type1","_id":"1"}}

  4. {"field1":"value1"}

  5. # 写入时不指定_id

  6. POST _bulk

  7. {"index":{"_index":"test","_type":"type1"}}

  8. {"field1":"value1"}


8. 使用routing

对于数据量较大的index,一般会配置多个shard来分摊压力。这种场景下,一个查询会同时搜索所有的shard,然后再将各个shard的结果合并后,返回给用户。对于高并发的小查询场景,每个分片通常仅抓取极少量数据,此时查询过程中的调度开销远大于实际读取数据的开销,且查询速度取决于最慢的一个分片。开启routing功能后,ES会将routing相同的数据写入到同一个分片中(也可以是多个,由index.routingpartitionsize参数控制)。如果查询时指定routing,那么ES只会查询routing指向的那个分片,可显著降低调度开销,提升查询效率。 routing的使用方式如下:

  1. # 写入

  2. PUT my_index/my_type/1?routing=user1

  3. {

  4.  "title":"This is a document"

  5. }

  6. # 查询

  7. GET my_index/_search?routing=user1,user2

  8. {

  9.  "query":{

  10.    "match":{

  11.      "title":"document"

  12.    }

  13.  }

  14. }


9. 为string类型的字段选取合适的存储方式

  • 存为text类型的字段(string字段默认类型为text): 做分词后存储倒排索引,支持全文检索,可以通过下面几个参数优化其存储方式:

    • norms:用于在搜索时计算该doc的_score(代表这条数据与搜索条件的相关度),如果不需要评分,可以将其关闭。

    • indexoptions:控制倒排索引中包括哪些信息(docs、freqs、positions、offsets)。对于不太注重score/highlighting的使用场景,可以设为 docs来降低内存/磁盘资源消耗。

    • fields: 用于添加子字段。对于有sort和聚合查询需求的场景,可以添加一个keyword子字段以支持这两种功能。

  1. PUT my_index

  2. {

  3.  "mappings":{

  4.    "my_type":{

  5.      "properties":{

  6.        "title":{

  7.          "type":"text",

  8.          "norms":false,

  9.          "index_options":"docs",

  10.          "fields":{

  11.            "raw":{

  12.              "type": "keyword"

  13.            }

  14.          }

  15.        }

  16.      }

  17.    }

  18.  }

  19. }

  • 存为keyword类型的字段: 不做分词,不支持全文检索。text分词消耗CPU资源,冗余存储keyword子字段占用存储空间。如果没有全文索引需求,只是要通过整个字段做搜索,可以设置该字段的类型为keyword,提升写入速率,降低存储成本。 设置字段类型的方法有两种:一是创建一个具体的index时,指定字段的类型;二是通过创建template,控制某一类index的字段类型。

  1. # 1. 通过mapping指定 tags 字段为keyword类型

  2. PUT my_index

  3. {

  4.  "mappings":{

  5.    "my_type":{

  6.      "properties":{

  7.        "tags":{

  8.          "type": "keyword"

  9.        }

  10.      }

  11.    }

  12.  }

  13. }

  14. # 2. 通过template,指定my_index*类的index,其所有string字段默认为keyword类型

  15. PUT _template/my_template

  16. {

  17.    "order":0,

  18.    "template":"my_index*",

  19.    "mappings":{

  20.      "_default_":{

  21.        "dynamic_templates":[

  22.          {

  23.            "strings":{

  24.              "match_mapping_type":"string",

  25.              "mapping":{

  26.                "type":"keyword",

  27.                "ignore_above":256

  28.              }

  29.            }

  30.          }

  31.        ]

  32.      }

  33.    },

  34.    "aliases":{}

  35.  }


10. 查询时,使用query-bool-filter组合取代普通query

默认情况下,ES通过一定的算法计算返回的每条数据与查询语句的相关度,并通过score字段来表征。但对于非全文索引的使用场景,用户并不care查询结果与查询条件的相关度,只是想精确的查找目标数据。此时,可以通过query-bool-filter组合来让ES不计算score,并且尽可能的缓存filter的结果集,供后续包含相同filter的查询使用,提高查询效率。

  1. # 普通查询

  2. POST my_index/_search

  3. {

  4.  "query":{

  5.    "term":{"user":"Kimchy"}

  6.  }

  7. }

  8. # query-bool-filter 加速查询

  9. POST my_index/_search

  10. {

  11.  "query":{

  12.    "bool":{

  13.      "filter":{

  14.        "term":{"user":"Kimchy"}

  15.      }

  16.    }

  17.  }

  18. }


11. index按日期滚动,便于管理

写入ES的数据最好通过某种方式做分割,存入不同的index。常见的做法是将数据按模块/功能分类,写入不同的index,然后按照时间去滚动生成index。这样做的好处是各种数据分开管理不会混淆,也易于提高查询效率。同时index按时间滚动,数据过期时删除整个index,要比一条条删除数据或deletebyquery效率高很多,因为删除整个index是直接删除底层文件,而deletebyquery是查询-标记-删除。

举例说明,假如有[modulea,moduleb]两个模块产生的数据,那么index规划可以是这样的:一类index名称是modulea + {日期},另一类index名称是module_b+ {日期}。对于名字中的日期,可以在写入数据时自己指定精确的日期,也可以通过ES的ingest pipeline中的 index-name-processor实现(会有写入性能损耗)。

  1. # module_a 类index

  2. -创建index:

  3. PUT module_a@2018_01_01

  4. {

  5.    "settings":{

  6.        "index":{

  7.            "number_of_shards":3,

  8.            "number_of_replicas":2

  9.        }

  10.    }

  11. }

  12. PUT module_a@2018_01_02

  13. {

  14.    "settings":{

  15.        "index":{

  16.            "number_of_shards":3,

  17.            "number_of_replicas":2

  18.        }

  19.    }

  20. }

  21. ...

  22. -查询数据:

  23. GET module_a@*/_search

  24. #  module_b 类index

  25. -创建index:

  26. PUT module_b@2018_01_01

  27. {

  28.    "settings":{

  29.        "index":{

  30.            "number_of_shards":3,

  31.            "number_of_replicas":2

  32.        }

  33.    }

  34. }

  35. PUT module_b@2018_01_02

  36. {

  37.    "settings":{

  38.        "index":{

  39.            "number_of_shards":3,

  40.            "number_of_replicas":2

  41.        }

  42.    }

  43. }

  44. ...

  45. -查询数据:

  46. GET module_b@*/_search


12. 按需控制index的分片数和副本数

分片(shard):一个ES的index由多个shard组成,每个shard承载index的一部分数据。

副本(replica):index也可以设定副本数(numberofreplicas),也就是同一个shard有多少个备份。对于查询压力较大的index,可以考虑提高副本数(numberofreplicas),通过多个副本均摊查询压力。

shard数量(numberofshards)设置过多或过低都会引发一些问题:shard数量过多,则批量写入/查询请求被分割为过多的子写入/查询,导致该index的写入、查询拒绝率上升;对于数据量较大的inex,当其shard数量过小时,无法充分利用节点资源,造成机器资源利用率不高 或 不均衡,影响写入/查询的效率。

对于每个index的shard数量,可以根据数据总量、写入压力、节点数量等综合考量后设定,然后根据数据增长状态定期检测下shard数量是否合理。基础架构部数据库团队的推荐方案是:

  • 对于数据量较小(100GB以下)的index,往往写入压力查询压力相对较低,一般设置3~5个shard,numberofreplicas设置为1即可(也就是一主一从,共两副本) 。

  • 对于数据量较大(100GB以上)的index:

    • 一般把单个shard的数据量控制在(20GB~50GB)

    • 让index压力分摊至多个节点:可通过index.routing.allocation.totalshardsper_node参数,强制限定一个节点上该index的shard数量,让shard尽量分配到不同节点上

    • 综合考虑整个index的shard数量,如果shard数量(不包括副本)超过50个,就很可能引发拒绝率上升的问题,此时可考虑把该index拆分为多个独立的index,分摊数据量,同时配合routing使用,降低每个查询需要访问的shard数量。


稳定性调优

一 Linux参数调优


  1. # 修改系统资源限制

  2. # 单用户可以打开的最大文件数量,可以设置为官方推荐的65536或更大些

  3. echo"* - nofile 655360">>/etc/security/limits.conf

  4. # 单用户内存地址空间

  5. echo"* - as unlimited">>/etc/security/limits.conf

  6. # 单用户线程数

  7. echo"* - nproc 2056474">>/etc/security/limits.conf

  8. # 单用户文件大小

  9. echo"* - fsize unlimited">>/etc/security/limits.conf

  10. # 单用户锁定内存

  11. echo"* - memlock unlimited">>/etc/security/limits.conf

  12. # 单进程可以使用的最大map内存区域数量

  13. echo"vm.max_map_count = 655300">>/etc/sysctl.conf

  14. # TCP全连接队列参数设置, 这样设置的目的是防止节点数较多(比如超过100)的ES集群中,节点异常重启时全连接队列在启动瞬间打满,造成节点hang住,整个集群响应迟滞的情况

  15. echo"net.ipv4.tcp_abort_on_overflow = 1">>/etc/sysctl.conf

  16. echo"net.core.somaxconn = 2048">>/etc/sysctl.conf

  17. # 降低tcp alive time,防止无效链接占用链接数

  18. echo300>/proc/sys/net/ipv4/tcp_keepalive_time


二 ES节点配置


1. jvm.options

-Xms和-Xmx设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给系统cache使用。

  • jvm内存建议不要低于2G,否则有可能因为内存不足导致ES无法正常启动或OOM

  • jvm建议不要超过32G,否则jvm会禁用内存对象指针压缩技术,造成内存浪费


2. elasticsearch.yml

  • 设置内存熔断参数,防止写入或查询压力过高导致OOM,具体数值可根据使用场景调整。 indices.breaker.total.limit: 30% indices.breaker.request.limit: 6% indices.breaker.fielddata.limit: 3%


  • 调小查询使用的cache,避免cache占用过多的jvm内存,具体数值可根据使用场景调整。 indices.queries.cache.count: 500 indices.queries.cache.size: 5%


  • 单机多节点时,主从shard分配以ip为依据,分配到不同的机器上,避免单机挂掉导致数据丢失。 cluster.routing.allocation.awareness.attributes: ip node.attr.ip: 1.1.1.1


三 ES使用方式


1. 节点数较多的集群,增加专有master,提升集群稳定性

ES集群的元信息管理、index的增删操作、节点的加入剔除等集群管理的任务都是由master节点来负责的,master节点定期将最新的集群状态广播至各个节点。所以,master的稳定性对于集群整体的稳定性是至关重要的。当集群的节点数量较大时(比如超过30个节点),集群的管理工作会变得复杂很多。此时应该创建专有master节点,这些节点只负责集群管理,不存储数据,不承担数据读写压力;其他节点则仅负责数据读写,不负责集群管理的工作。

这样把集群管理和数据的写入/查询分离,互不影响,防止因读写压力过大造成集群整体不稳定。 将专有master节点和数据节点的分离,需要修改ES的配置文件,然后滚动重启各个节点。

  1. # 专有master节点的配置文件(conf/elasticsearch.yml)增加如下属性:

  2. node.master:true

  3. node.data:false

  4. node.ingest:false

  5. # 数据节点的配置文件增加如下属性(与上面的属性相反):

  6. node.master:false

  7. node.data:true

  8. node.ingest:true


2. 控制index、shard总数量

上面提到,ES的元信息由master节点管理,定期同步给各个节点,也就是每个节点都会存储一份。这个元信息主要存储在clusterstate中,如所有node元信息(indices、节点各种统计参数)、所有index/shard的元信息(mapping, location, size)、元数据ingest等。

ES在创建新分片时,要根据现有的分片分布情况指定分片分配策略,从而使各个节点上的分片数基本一致,此过程中就需要深入遍历clusterstate。当集群中的index/shard过多时,clusterstate结构会变得过于复杂,导致遍历clusterstate效率低下,集群响应迟滞。基础架构部数据库团队曾经在一个20个节点的集群里,创建了4w+个shard,导致新建一个index需要60s+才能完成。 当index/shard数量过多时,可以考虑从以下几方面改进:

  • 降低数据量较小的index的shard数量

  • 把一些有关联的index合并成一个index

  • 数据按某个维度做拆分,写入多个集群


3. Segment Memory优化

前面提到,ES底层采用Lucene做存储,而Lucene的一个index又由若干segment组成,每个segment都会建立自己的倒排索引用于数据查询。Lucene为了加速查询,为每个segment的倒排做了一层前缀索引,这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。Lucene加载segment的时候将其全量装载到内存中,加快查询速度。这部分内存被称为SegmentMemory, 常驻内存,占用heap,无法被GC

前面提到,为利用JVM的对象指针压缩技术来节约内存,通常建议JVM内存分配不要超过32G。当集群的数据量过大时,SegmentMemory会吃掉大量的堆内存,而JVM内存空间又有限,此时就需要想办法降低SegmentMemory的使用量了,常用方法有下面几个:

  • 定期删除不使用的index

  • 对于不常访问的index,可以通过close接口将其关闭,用到时再打开

  • 通过force_merge接口强制合并segment,降低segment数量

基础架构部数据库团队在此基础上,对FST部分进行了优化,释放高达40%的Segment Memory内存空间。


高可用Hadoop平台-Flume NG实战图解篇 - 哥不是小萝莉 - 博客园

$
0
0

1.概述

  今天补充一篇关于Flume的博客,前面在讲解高可用的Hadoop平台的时候遗漏了这篇,本篇博客为大家讲述以下内容:

  • Flume NG简述
  • 单点Flume NG搭建、运行
  • 高可用Flume NG搭建
  • Failover测试
  • 截图预览

  下面开始今天的博客介绍。

2.Flume NG简述

  Flume NG是一个分布式,高可用,可靠的系统,它能将不同的海量数据收集,移动并存储到一个数据存储系统中。轻量,配置简单,适用于各种日志收集,并支持Failover和负载均衡。并且它拥有非常丰富的组件。Flume NG采用的是三层架构:Agent层,Collector层和Store层,每一层均可水平拓展。其中Agent包含Source,Channel和Sink,三者组建了一个Agent。三者的职责如下所示:

  • Source:用来消费(收集)数据源到Channel组件中
  • Channel:中转临时存储,保存所有Source组件信息
  • Sink:从Channel中读取,读取成功后会删除Channel中的信息

  下图是Flume NG的架构图,如下所示:

  图中描述了,从外部系统(Web Server)中收集产生的日志,然后通过Flume的Agent的Source组件将数据发送到临时存储Channel组件,最后传递给Sink组件,Sink组件直接把数据存储到HDFS文件系统中。

3.单点Flume NG搭建、运行

  我们在熟悉了Flume NG的架构后,我们先搭建一个单点Flume收集信息到HDFS集群中,由于资源有限,本次直接在之前的高可用Hadoop集群上搭建Flume。

  场景如下:在NNA节点上搭建一个Flume NG,将本地日志收集到HDFS集群。

3.1基础软件

  在搭建Flume NG之前,我们需要准备必要的软件,具体下载地址如下所示:

  JDK由于之前在安装Hadoop集群时已经配置过,这里就不赘述了,若需要配置的同学,可参考《 配置高可用的Hadoop平台》。

3.2安装与配置

  • 安装

  首先,我们解压flume安装包,命令如下所示:

[hadoop@nna ~]$tar-zxvf apache-flume-1.5.2-bin.tar.gz
  • 配置

  环境变量配置内容如下所示:

export FLUME_HOME=/home/hadoop/flume-1.5.2export PATH=$PATH:$FLUME_HOME/bin

  flume-conf.properties

#agent1 name
agent1.sources=source1
agent1.sinks=sink1
agent1.channels=channel1


#Spooling Directory
#set source1
agent1.sources.source1.type=spooldir
agent1.sources.source1.spoolDir=/home/hadoop/dir/logdfs
agent1.sources.source1.channels=channel1
agent1.sources.source1.fileHeader=falseagent1.sources.source1.interceptors=i1
agent1.sources.source1.interceptors.i1.type=timestamp

#set sink1
agent1.sinks.sink1.type=hdfs
agent1.sinks.sink1.hdfs.path=/home/hdfs/flume/logdfs
agent1.sinks.sink1.hdfs.fileType=DataStream
agent1.sinks.sink1.hdfs.writeFormat=TEXT
agent1.sinks.sink1.hdfs.rollInterval=1agent1.sinks.sink1.channel=channel1
agent1.sinks.sink1.hdfs.filePrefix=%Y-%m-%d

#set channel1
agent1.channels.channel1.type=fileagent1.channels.channel1.checkpointDir=/home/hadoop/dir/logdfstmp/point
agent1.channels.channel1.dataDirs=/home/hadoop/dir/logdfstmp

  flume-env.sh

JAVA_HOME=/usr/java/jdk1.7

  注:配置中的目录若不存在,需提前创建。

3.3启动

  启动命令如下所示:

flume-ng agent -n agent1 -c conf -f flume-conf.properties -Dflume.root.logger=DEBUG,console

  注:命令中的agent1表示配置文件中的Agent的Name,如配置文件中的agent1。flume-conf.properties表示配置文件所在配置,需填写准确的配置文件路径。

3.4效果预览

  之后,成功上传后本地目的会被标记完成。如下图所示:

 4.高可用Flume NG搭建

  在完成单点的Flume NG搭建后,下面我们搭建一个高可用的Flume NG集群,架构图如下所示:

  图中,我们可以看出,Flume的存储可以支持多种,这里只列举了HDFS和Kafka(如:存储最新的一周日志,并给Storm系统提供实时日志流)。

4.1节点分配

  Flume的Agent和Collector分布如下表所示:

名称 HOST角色
Agent110.211.55.14Web Server
Agent210.211.55.15Web Server
Agent310.211.55.16 Web Server
Collector110.211.55.18AgentMstr1
Collector210.211.55.19AgentMstr2

 

  图中所示,Agent1,Agent2,Agent3数据分别流入到Collector1和Collector2,Flume NG本身提供了Failover机制,可以自动切换和恢复。在上图中,有3个产生日志服务器分布在不同的机房,要把所有的日志都收集到一个集群中存储。下面我们开发配置Flume NG集群

4.2配置

  在下面单点Flume中,基本配置都完成了,我们只需要新添加两个配置文件,它们是flume-client.properties和flume-server.properties,其配置内容如下所示:

  • flume-client.properties
#agent1 name
agent1.channels=c1
agent1.sources=r1
agent1.sinks=k1 k2

#set gruop
agent1.sinkgroups=g1 

#set channel
agent1.channels.c1.type=memory
agent1.channels.c1.capacity=1000agent1.channels.c1.transactionCapacity=100agent1.sources.r1.channels=c1
agent1.sources.r1.type=exec
agent1.sources.r1.command=tail-F /home/hadoop/dir/logdfs/test.log

agent1.sources.r1.interceptors=i1 i2
agent1.sources.r1.interceptors.i1.type=static
agent1.sources.r1.interceptors.i1.key=Type
agent1.sources.r1.interceptors.i1.value=LOGIN
agent1.sources.r1.interceptors.i2.type=timestamp

# set sink1
agent1.sinks.k1.channel=c1
agent1.sinks.k1.type=avro
agent1.sinks.k1.hostname=nna
agent1.sinks.k1.port=52020# set sink2
agent1.sinks.k2.channel=c1
agent1.sinks.k2.type=avro
agent1.sinks.k2.hostname=nns
agent1.sinks.k2.port=52020#set sink group
agent1.sinkgroups.g1.sinks=k1 k2

#set failover
agent1.sinkgroups.g1.processor.type=failover
agent1.sinkgroups.g1.processor.priority.k1=10agent1.sinkgroups.g1.processor.priority.k2=1agent1.sinkgroups.g1.processor.maxpenalty=10000

  注:指定Collector的IP和Port。

  • flume-server.properties
#set Agent name
a1.sources=r1
a1.channels=c1
a1.sinks=k1

#set channel
a1.channels.c1.type=memory
a1.channels.c1.capacity=1000a1.channels.c1.transactionCapacity=100# other node,nna to nns
a1.sources.r1.type=avro
a1.sources.r1.bind=nna
a1.sources.r1.port=52020a1.sources.r1.interceptors=i1
a1.sources.r1.interceptors.i1.type=static
a1.sources.r1.interceptors.i1.key=Collector
a1.sources.r1.interceptors.i1.value=NNA
a1.sources.r1.channels=c1

#set sink to hdfs
a1.sinks.k1.type=hdfs
a1.sinks.k1.hdfs.path=/home/hdfs/flume/logdfs
a1.sinks.k1.hdfs.fileType=DataStream
a1.sinks.k1.hdfs.writeFormat=TEXT
a1.sinks.k1.hdfs.rollInterval=1a1.sinks.k1.channel=c1
a1.sinks.k1.hdfs.filePrefix=%Y-%m-%d

  注:在另一台Collector节点上修改IP,如在NNS节点将绑定的对象有nna修改为nns。

4.3启动

  在Agent节点上启动命令如下所示:

flume-ng agent -n agent1 -c conf -f flume-client.properties -Dflume.root.logger=DEBUG,console

  注:命令中的agent1表示配置文件中的Agent的Name,如配置文件中的agent1。flume-client.properties表示配置文件所在配置,需填写准确的配置文件路径。

  在Collector节点上启动命令如下所示:

flume-ng agent -n a1 -c conf -f flume-server.properties -Dflume.root.logger=DEBUG,console

  注:命令中的a1表示配置文件中的Agent的Name,如配置文件中的a1。flume-server.properties表示配置文件所在配置,需填写准确的配置文件路径。

5.Failover测试

  下面我们来测试下Flume NG集群的高可用(故障转移)。场景如下:我们在Agent1节点上传文件,由于我们配置Collector1的权重比Collector2大,所以Collector1优先采集并上传到存储系统。然后我们kill掉Collector1,此时有Collector2负责日志的采集上传工作,之后,我们手动恢复Collector1节点的Flume服务,再次在Agent1上次文件,发现Collector1恢复优先级别的采集工作。具体截图如下所示:

  • Collector1优先上传

  • HDFS集群中上传的log内容预览

  • Collector1宕机,Collector2获取优先上传权限

  • 重启Collector1服务,Collector1重新获得优先上传的权限

6.截图预览

  下面为大家附上HDFS文件系统中的截图预览,如下图所示:

  • HDFS文件系统中的文件预览

  • 上传的文件内容预览

7.总结

  在配置高可用的Flume NG时,需要注意一些事项。在Agent中需要绑定对应的Collector1和Collector2的IP和Port,另外,在配置Collector节点时,需要修改当前Flume节点的配置文件,Bind的IP(或HostName)为当前节点的IP(或HostName),最后,在启动的时候,指定配置文件中的Agent的Name和配置文件的路径,否则会出错。

8.结束语

  这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

30个MySQL千万级大数据SQL查询优化技巧详解

$
0
0

本文总结了30个mysql千万级大数据SQL查询优化技巧,特别适合大

数据里的MYSQL使用。

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num=0

3.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。

4.应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num=10 or num=20可以这样查询:select id from t where num=10 union all select id from t where num=20

5.in 和 not in 也要慎用,否则会导致全表扫描,如:select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了:select id from t where num between 1 and 3

6.下面的查询也将导致全表扫描:select id from t where name like ‘%李%'若要提高效率,可以考虑全文检索。

7. 如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:select id from t where num=@num可以改为强制查询使用索引:select id from t with(index(索引名)) where num=@num

8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where num/2=100应改为:select id from t where num=100*2

9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where substring(name,1,3)='abc' ,name以abc开头的id应改为:

select id from t where name like ‘abc%'

10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。

12.不要写一些没有意义的查询,如需要生成一个空表结构:select col1,col2 into #t from t where 1=0

这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:

create table #t(…)

13.很多时候用 exists 代替 in 是一个好的选择:select num from a where num in(select num from b)

用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num)

14.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

15. 索引并不是越多越好,索引固然可 以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

16. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

19.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

21.避免频繁创建和删除临时表,以减少系统表资源的消耗。

22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。

23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

27. 与临时表一样,游标并不是不可使 用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC 消息。

29.尽量避免大事务操作,提高系统并发能力。

30.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

如果你的程序都能满足这30条的话那么你的程序执行效率会有很大的提高

哈佛招生机密曝光 秘密

$
0
0

一名亚裔高中生,在SAT三门类别考试、9门AP考试上都拥有完美的分数,在高中592名学生中排名第一。就连审查面试他的哈佛行政官员都将他称为“尖子生”,并开玩笑说可能要和普林斯顿大学竞争这名学生。然而这样一名出色的学生最终并没有拿到哈佛的录取通知书。大家觉得会是什么原因呢?

当哈佛大学的招生文件在法庭上被公开后,相信许多曾经才华出众却被哈佛拒之门外的申请人才真正明白, 他们与哈佛的距离可能还有些其他东西

哈佛大学表示,他们每年都在努力建立一届“公民和公民领袖”构成的多元化学生,他们将帮助塑造社会的未来。 GRETCHEN ERTL FOR THE NEW YORK TIMES

根据法院文件所描述的,在哈佛招生过程中,招生官们有一套属于自己的 “秘密语言”,包括 “dockets,” “the lop list,” “tips,” “DE,” “Z-list” 以及 “dean’s interest list” 。除了这些,他们还拥有一个筛选系统,申请人来自哪里,父母是否为哈佛校友,家中财务状况,是否符合学校多样性目标等等都非常重要,甚至与SAT考满分的重要度相同。

“dockets待审表”—入学筛选开始后,招生官将把所有申请人通过地理位置分为20个“待审表”,每张表都分配给一个招生官委员会。该委员会通常对这个地区以及高中有着深入了解。他们将从5个方面对申请人进行评定,包括学术、课外、体育、个性以及综合。

“tips 小奖励”—招生优势。如果你符合哈佛的5个招生优势其中一个,那么你的机会就比普通申请者高出许多。其中包括:少数民族;哈佛、Radcliffe校友子女;哈佛捐助者的亲属;工作人员教职员工的女子;特招运动生。然而报告中显示,哈佛并没有给予亚裔任何招生优势。

“DE-区分卓越”—DE通常用在区分卓越上,也就是distinguishing excellence。一种“讽刺性赞美”,例如——虽然这个人努力工作,但她真的会放松自己享受生活乐趣吗?通常招生官会剥夺学术成绩非常优秀,但缺乏DE的学生的优先权。

‘’The Lop List 剥夺优先权”——这也是许多亚裔学生最常被坑的一个环节,哈佛的个人评级会考虑到申请人的性格(这也是哈佛最可疑的招生标准),而亚裔通常被描述为勤劳、聪明,但令人难以区分、不够凸出,令人回忆起痛苦的刻板印象。如果被标上了DE,那么就会进入the lop list。

“dean’s interest list 主任/院长关注名单”—这份名单并不是我们想象中的那种成绩优秀,受到院长青睐的学生名单。而是与捐赠者有利益关系,或者与哈佛有某种关系的申请者名单。

其中最鲜为人知的,就是 Z名单,也就是哈佛大学走后门的名单。目前哈佛大学对Z名单保持沉默,而且递交给法院的大部分相关信息都经过了涂改。

根据原告所述,这份名单上的申请人成绩介于合格与不合格的边缘,但又是哈佛想要录取的申请者。这些人可能由于财力、背景过人。名单上的申请者可以在推迟一年的条件下保证被录取。(相当于是一张保送哈佛的名单)

在2014年到2019年期间,每年约有50-60名学生通过Z名单被录取,大部分都是白人学生,特征为家里人是哈佛校友,或者是院长主任希望录取的学生。这几周法院已经要求哈佛大学必须递交详细的Z名单,也就是将之前涂改的部分完整呈现出来。

【挖掘模型】:Python-DBSCAN算法 - 简书

$
0
0

数据源:data (7).csv

data (7).csv

DBSCAN算法结果

DBSCAN模型

DBSCAN原理

# DBSCAN算法:将簇定义为密度相连的点最大集合,能够把具有足够高密度的区域划分为簇,并且可在噪声的空间数据集中发现任意形状的簇。
    # 密度:空间中任意一点的密度是以该点为圆心,以EPS为半径的圆区域内包含的点数目
    # 边界点:空间中某一点的密度,如果小于某一点给定的阈值minpts,则称为边界点
    # 噪声点:不属于核心点,也不属于边界点的点,也就是密度为1的点
# API:
    # model = sklearn.cluster.DBSCAN(eps_领域大小圆半径,min_samples_领域内,点的个数的阈值)
    # model.fit(data) 训练模型
    # model.fit_predict(data) 模型的预测方法

DBSCAN代码-A

import pandas
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
#导入数据
data = pandas.read_csv("F:\\python 数据挖掘分析实战\\Data\\data (7).csv")

eps = 0.2;
MinPts = 5;

model = DBSCAN(eps, MinPts)

model.fit(data)

data['type'] = model.fit_predict(data)

plt.scatter(
   data['x'], 
   data['y'],
   c=data['type']
)

DBSCAN代码-B

import numpy
import pandas
import matplotlib.pyplot as plt

#导入数据
data = pandas.read_csv("F:\\python 数据挖掘分析实战\\Data\\data (7).csv")

plt.scatter(
   data['x'], 
   data['y']
)

eps = 0.2;
MinPts = 5;

from sklearn.metrics.pairwise import euclidean_distances

ptses = []
dist = euclidean_distances(data)
for row in dist:
   #密度,空间中任意一点的密度是以该点为圆心、以 Eps 为半径的圆区域内包含的点数
   density = numpy.sum(row<eps)
   pts = 0;
   if density>MinPts:
       #核心点(Core Points)
       #空间中某一点的密度,如果大于某一给定阈值MinPts,则称该为核心点
       pts = 1
   elif density>1 :
       #边界点(Border Points)
       #空间中某一点的密度,如果小于某一给定阈值MinPts,则称该为边界点
       pts = 2
   else:
       #噪声点(Noise Points)
       #数据集中不属于核心点,也不属于边界点的点,也就是密度值为1的点
       pts = 0
   ptses.append(pts)

#把噪声点过滤掉,因为噪声点无法聚类,它们独自一类
corePoints = data[pandas.Series(ptses)!=0]

coreDist = euclidean_distances(corePoints)

#首先,把每个点的领域都作为一类
#邻域(Neighborhood)
#空间中任意一点的邻域是以该点为圆心、以 Eps 为半径的圆区域内包含的点集合
cluster = dict();
i = 0;
for row in coreDist: 
   cluster[i] = numpy.where(row<eps)[0]
   i = i + 1

#然后,将有交集的领域,都合并为新的领域
for i in range(len(cluster)):
   for j in range(len(cluster)):
       if len(set(cluster[j]) & set(cluster[i]))>0 and i!=j:
           cluster[i] = list(set(cluster[i]) | set(cluster[j]))
           cluster[j] = list();

#最后,找出独立(也就是没有交集)的领域,就是我们最后的聚类的结果了
result = dict();
j = 0
for i in range(len(cluster)):
 if len(cluster[i])>0:
   result[j] = cluster[i]
   j = j + 1

#找出每个点所在领域的序号,作为他们最后聚类的结果标记
for i in range(len(result)):
   for j in result[i]:
       data.at[j, 'type'] = i

plt.scatter(
   data['x'], 
   data['y'],
   c=data['type']
)

参考文献
作者A: ken

GitHub - allwefantasy/streamingpro: Build Spark Batch/Streaming/MLlib Application by SQL

$
0
0

StreamingPro 中文文档

  1. 五分钟快速上手和体验
  2. Five Minute Quick Tutorial

应用模式和服务模式

  1. 应用模式:写json配置文件,StreamingPro启动后执行该文件,可以作为批处理或者流式程序。
  2. 服务模式:启动一个StreamingPro Server作为常驻程序,然后通过http接口发送MLSQL脚本进行交互。

我们强烈推荐使用第二种模式,第一种模式现在已经不太更新了,现在迅速迭代的是第二种模式,并且第二种模式可以构建AI平台。 为了避免编译的麻烦,你可以直接使用 release版本

对于确实需要使用json配置文件的,我们也提供了batch.mlsql脚本,可以让你使用mlsql语法,例如(v1.1.2开始具有这个功能):

{"mlsql": {"desc":"测试","strategy":"spark","algorithm": [],"ref": [],"compositor": [
      {"name":"batch.mlsql","params": [
          {"sql": ["select 'a' as a as table1;","save overwrite table1 as parquet.`/tmp/kk`;"]
          }
        ]
      }
    ],"configParams": {
    }
  }
}

编译

高级编程

使用MLSQL做机器学习

部署模型API服务

MLSQL常用功能

使用配置完成Spark编程

  1. Spark 批处理
  2. Spark Streaming
  3. Structured Streaming
  4. StreamingPro对机器学习的支持

概览:

  1. 概述
  2. 项目模块说明
  3. 编译
  4. 相关概念
  5. StreamingPro的一些参数

周边工具

  1. StreamingPro Manager
  2. StreamingPro json文件编辑器支持

实验

  1. flink支持

其他文档

概述

StreamingPro 支持以Spark,Flink等作为底层分布式计算引擎,通过一套统一的配置文件完成批处理,流式计算,Rest服务的开发。 特点有:

  1. 使用Json描述文件完成流式,批处理的开发,不用写代码。
  2. 支持SQL Server,支持XSQL/MLSQL(重点),完成批处理,机器学习,即席查询等功能。
  3. 标准化输入输出,支持UDF函数注册,支持自定义模块开发
  4. 支持Web化管理Spark应用的启动,监控

如果更细节好处有:

  1. 跨版本:StreamingPro可以让你不用任何变更就可以轻易的运行在spark 1.6/2.1/2.2上。
  2. 新语法:提供了新的DSl查询语法/Json配置语法
  3. 程序的管理工具:提供web界面启动/监控 Spark 程序
  4. 功能增强:2.1之后Structured Streaming 不支持kafka 0.8/0.9 ,Structured,此外还有比如spark streaming 支持offset 保存等
  5. 简化Spark SQL Server搭建成本:提供rest接口/thrift 接口,支持spark sql server 的负载均衡,自动将driver 注册到zookeeper上
  6. 探索更多的吧

项目模块说明

模块名描述备注
streamingpro-commons一些基础工具类
streamingpro-spark-commonSpark有多个版本,所以可以共享一些基础的东西
streamingpro-flinkstreamingpro对flink的支持
streamingpro-sparkstreamingpro对spark 1.6.x的支持
streamingpro-mlsqlstreamingpro对spark 2.x的支持(主项目)
streamingpro-apistreamingpro把底层的spark API暴露出来,方便用户灵活处理问题
streamingpro-manager通过该模块,可以很方便的通过web界面启动,管理,监控 spark相关的应用
streamingpro-dls自定义connect,load,select,save,train,register等语法,便于用类似sql的方式做批处理任务,机器学习等

相关概念

如果你使用StreamingPro,那么所有的工作都是在编辑一个Json配置文件。通常一个处理流程,会包含三个概念:

  1. 多个输入
  2. 多个连续/并行的数据处理
  3. 多个输出

StreamingPro会通过'compositor'的概念来描述他们,你可以理解为一个处理单元。一个典型的输入compositor如下:

{"name": "batch.sources","params": [
          {"path": "file:///tmp/hdfsfile/abc.txt","format": "json","outputTable": "test"

          },
           {
              "path": "file:///tmp/parquet/","format": "parquet","outputTable": "test2"

            }
        ]
}

batch.sources就是一个compositor的名字。 这个compositor 把一个本地磁盘的文件映射成了一张表,并且告知系统,abc.txt里的内容 是json格式的。这样,我们在后续的compositor模块就可以使用这个 test表名了。通常,StreamingPro希望整个处理流程, 也就是不同的compositor都采用表来进行衔接。

StreamingPro不仅仅能做批处理,还能做流式,流式支持Spark Streaming,Structured Streaming。依然以输入compositor为例,假设 我们使用的是Structured Streaming,则可以如下配置。

{"name": "ss.sources","params": [
          {"format": "kafka9","outputTable": "test","kafka.bootstrap.servers": "127.0.0.1:9092","topics": "test","path": "-"
          },
          {"format": "com.databricks.spark.csv","outputTable": "sample","header": "true","path": "/Users/allwefantasy/streamingpro/sample.csv"
          }
        ]
      }

第一个表示我们对接的数据源是kafka 0.9,我们把Kafka的数据映射成表test。 因为我们可能还需要一些元数据,比如ip和城市的映射关系, 所以我们还可以配置一些其他的非流式的数据源,我们这里配置了一个smaple.csv文件,并且命名为表sample。

如果你使用的是kafka >= 1.0,则 topics 参数需要换成'subscribe',并且使用时可能需要对内容做下转换,类似:

select CAST(key AS STRING) as k, CAST(value AS STRING) as v from test

启动时,你需要把-streaming.platform 设置为 ss

如果我们的输入输出都是Hive的话,可能就不需要batch.sources/batch.outputs 等组件了,通常一个batch.sql就够了。比如:

"without-sources-job": {"desc": "-","strategy": "spark","algorithm": [],"ref": [],"compositor": [
      {"name": "batch.sql","params": [
          {"sql": "select * from hiveTable","outputTableName": "puarquetTable"
          }
        ]
      },
      {"name": "batch.outputs","params": [
          {"format": "parquet","inputTableName": "puarquetTable","path": "/tmp/wow","mode": "Overwrite"
          }
        ]
      }
    ],"configParams": {
    }
  }

在批处理里,batch.sources/batch.outputs 都是可有可无的,但是对于流式程序,stream.sources/stream.outputs/ss.sources/ss.outputs 则是必须的。

StreamingPro的一些参数

Property NameDefaultMeaning
streaming.name(none) required等价于 spark.app.name
streaming.master(none) required等价于 spark.master
streaming.duration10 secondsspark streaming 周期,默认单位为秒
streaming.resttrue/false,default is false是否提供http接口
streaming.spark.servicetrue/false,default is false开启该选项时,streaming.platform必须为spark. 该选项会保证spark实例不会退出
streaming.platformspark/spark_streaming/ss/flink,default is spark基于什么平台跑
streaming.checkpoint(none)spark streaming checkpoint 目录
streaming.kafka.offsetPath(none)kafka的偏移量保存目录。如果没有设置,会保存在内存中
streaming.driver.port9003配置streaming.rest使用,streaming.rest为true,你可以设置一个http端口
streaming.spark.hadoop.*(none)hadoop configuration,eg. -streaming.spark.hadoop.fs.defaultFS hdfs://name:8020
streaming.job.file.path(none)配置文件路径,默认从hdfs加载
streaming.jobs(none)json配置文件里的job名称,按逗号分隔。如果没有配置该参数,默认运行所有job
streaming.zk.servers(none)如果把spark作为一个server,那么streamingpro会把driver地址注册到zookeeper上
streaming.zk.conf_root_dir(none)配置streaming.zk.servers使用
streaming.enableHiveSupportfalse是否支持Hive
streaming.thriftfalse是否thrift server
streaming.sql.source.[name].[参数](none)batch/ss/stream.sources 中,你可以替换里面的任何一个参数
streaming.sql.out.[name].[参数](none)batch/ss/stream.outputs 中,你可以替换里面的任何一个参数
streaming.sql.params.[param-name](none)batch/ss/stream.sql中,你是可以写表达式的,比如 select * from :table, 之后你可以通过命令行传递该table参数

后面三个参数值得进一步说明:

假设我们定义了两个数据源,firstSource,secondSource,描述如下:

{"name": "batch.sources","params": [
          {"name":"firstSource","path": "file:///tmp/sample_article.txt","format": "com.databricks.spark.csv","outputTable": "article","header":true
          },
          {"name":"secondSource","path": "file:///tmp/sample_article2.txt","format": "com.databricks.spark.csv","outputTable": "article2","header":true
            }
        ]
      }

我们希望path不是固定的,而是启动时候决定的,这个时候,我们可以在启动脚本中使用-streaming.sql.source.[name].[参数] 来完成这个需求。 比如:

-streaming.sql.source.firstSource.path  file:///tmp/wow.txt

这个时候,streamingpro启动的时候会动态将path 替换成你要的。包括outputTable等都是可以替换的。

有时候我们需要定时执行一个任务,而sql语句也是动态变化的,具体如下:

{"name": "batch.sql","params": [
          {"sql": "select * from test where hp_time=:today","outputTableName": "finalOutputTable"
          }
        ]
      },

这个时候我们在启动streamingpro的时候,通过参数:

-streaming.sql.params.today  "2017"

动态替换 sql语句里的:today

Viewing all 15907 articles
Browse latest View live