1.引言

最近接触到一个APP流量分析的项目,类似于友盟。涉及到几个C端(客户端)高并发的接口,这几个接口主要用于C端数据的提交。在没有任何缓冲的情况下,一个接口涉及到5张表的提交。压测的结果很不理想,原因有以下几点:

1.业务上处理原始数据,封装数据库对象,需要时间

2.服务与RDS的多张表进行交互

3.web接口同步处理业务

导致一台双核,16G机子,单实例,jdbc最大连接数100,吞吐量竟然只有50TPS。

能想到的改造方案就是引入一层缓冲,让C端接口不与RDS直接交互,也不直接处理业务,很自然就想到了rabbitmq,但是rabbitmq对分布式的支持比较一般,我们的数据体量也比较大,所以我们借鉴了友盟,引入了kafka,Kafka是一种高吞吐量的分布式发布订阅消息系统,起初在不做任何kafka优化的时候,简单地将C端提交的数据直接send到单节点kafka,就这样,我们的吞吐量达到了100TPS.还是有点小惊喜的。

最近一段时间研究了一下kafka,对一些参数进行调整,目前接口的吞吐量已经达到220TPS,写这篇文章主要想记录一下自己优化和部署经历。

2.kafka简介

kafka的结构图

这张图很好的诠释了kafka的结构,但是遗漏了一点,就是group的概念,我这里补充一下,一个组可以包含多个consumer对多个topic进行消费,但是不同组的消费都是独立的。

也就是说同一个topic的同一条消息可以被不同组的consumer消费。

我这里的主要的优化途径就是将kafka集群化,多partition化,使其并发度更高。

集群化都很好理解,那什么是多partition?

partition是topic的一个概念,即对topic进行分组,不同partition之间的消费相互独立,且有序。并且任意Partition在某一个时刻只能被一个Consumer Group内的一个Consumer消费,所以咯,假如topic只有一个partition的话,那么在一个Consumer Group内有效的消费者实例最多也就1个,并发度就会受限于kafka的partition数目。

上面都是讲消费,其实send操作也是一样的,要保证有序必然要等上一个发送ack之后,下一个发送才能进行,如果只有一个partition,那send之后的ack的等待时间必然会阻塞下面一次send,设计多个partition之后,可以同时往多个partition发送消息,自然吞吐量也就上去。

3.kafka集群的搭建以及参数配置

集群搭建

准备两台机子,然后去官网(http://kafka.apache.org/downloads)下载一个包。通过scp到服务器上,解压进入config目录,编辑server.config.

第一台机子配置(172.18.240.36):

broker.id=0  每台服务器的broker.id都不能相同

#hostname
host.name=172.18.240.36 #在log.retention.hours=168 下面新增下面三项
message.max.byte=5242880
default.replication.factor=2
replica.fetch.max.bytes=5242880 #设置zookeeper的连接端口
zookeeper.connect=172.18.240.36:4001
#默认partition数
num.partitions=2

第二台机子配置(172.18.240.62):

broker.id=1  每台服务器的broker.id都不能相同

#hostname
host.name=172.18.240.62 #在log.retention.hours=168 下面新增下面三项
message.max.byte=5242880
default.replication.factor=2
replica.fetch.max.bytes=5242880 #设置zookeeper的连接端口
zookeeper.connect=172.18.240.36:4001
#默认partition数
num.partitions=2

新增或者修改成以上配置。

对了,在此之前请先安装zookeeper,如果你用的是zookeeper集群的话,zookeeper.connect可以填写多个,中间用逗号隔开。

然后启动

nohup  ./kafka-server-start.sh ../config/server.properties 1>/dev/null 2>&1 &

测试一下:

在第一台机子上开启一个producer

./kafka-console-producer.sh --broker-list 172.18.240.36:9092 --topic test-test

在第二台机子上开启一个consumer

./kafka-console-consumer.sh --bootstrap-server 172.18.240.62:9092 --topic test-test --from-beginning

第一台机子发送一条消息

第二台机子立马收到消息

这样kafka的集群部署就完成了。就下来我们来看看,java的客户端代码如何编写。

4.kafka客户端代码示例

我这里的工程是建立在spring boot 之下的,仅供参考。

在 application.yml下添加如下配置:

kafka:
consumer:
default:
server: 172.18.240.36:9092,172.18.240.62:9092
enableAutoCommit: false
autoCommitIntervalMs: 100
sessionTimeoutMs: 15000
groupId: data_analysis_group
autoOffsetReset: latest
producer:
default:
server: 172.18.240.36:9092,172.18.240.62:9092
retries: 0
batchSize: 4096
lingerMs: 1
bufferMemory: 40960

添加两个配置类

package com.dtdream.analysis.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import java.util.HashMap;
import java.util.Map; @ConfigurationProperties(
prefix = "kafka.consumer.default"
)
@EnableKafka
@Configuration
public class KafkaConsumerConfig { private static final Logger log = LoggerFactory.getLogger(KafkaConsumerConfig.class);
private static String autoCommitIntervalMs; private static String sessionTimeoutMs; private static Class keyDeserializerClass = StringDeserializer.class; private static Class valueDeserializerClass = StringDeserializer.class; private static String groupId = "test-group"; private static String autoOffsetReset = "latest"; private static String server; private static boolean enableAutoCommit; public static String getServer() {
return server;
} public static void setServer(String server) {
KafkaConsumerConfig.server = server;
} public static boolean isEnableAutoCommit() {
return enableAutoCommit;
} public static void setEnableAutoCommit(boolean enableAutoCommit) {
KafkaConsumerConfig.enableAutoCommit = enableAutoCommit;
} public static String getAutoCommitIntervalMs() {
return autoCommitIntervalMs;
} public static void setAutoCommitIntervalMs(String autoCommitIntervalMs) {
KafkaConsumerConfig.autoCommitIntervalMs = autoCommitIntervalMs;
} public static String getSessionTimeoutMs() {
return sessionTimeoutMs;
} public static void setSessionTimeoutMs(String sessionTimeoutMs) {
KafkaConsumerConfig.sessionTimeoutMs = sessionTimeoutMs;
} public static Class getKeyDeserializerClass() {
return keyDeserializerClass;
} public static void setKeyDeserializerClass(Class keyDeserializerClass) {
KafkaConsumerConfig.keyDeserializerClass = keyDeserializerClass;
} public static Class getValueDeserializerClass() {
return valueDeserializerClass;
} public static void setValueDeserializerClass(Class valueDeserializerClass) {
KafkaConsumerConfig.valueDeserializerClass = valueDeserializerClass;
} public static String getGroupId() {
return groupId;
} public static void setGroupId(String groupId) {
KafkaConsumerConfig.groupId = groupId;
} public static String getAutoOffsetReset() {
return autoOffsetReset;
} public static void setAutoOffsetReset(String autoOffsetReset) {
KafkaConsumerConfig.autoOffsetReset = autoOffsetReset;
} @Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(10);
factory.getContainerProperties().setPollTimeout(3000);
factory.setRecordFilterStrategy(new RecordFilterStrategy<String, String>() {
@Override
public boolean filter(ConsumerRecord<String, String> consumerRecord) {
log.debug("partition is {},key is {},topic is {}",
consumerRecord.partition(), consumerRecord.key(), consumerRecord.topic());
return false;
}
});
return factory;
} private ConsumerFactory<String, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
} private Map<String, Object> consumerConfigs() {
Map<String, Object> propsMap = new HashMap<>();
propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, server);
propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs);
propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs);
propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, keyDeserializerClass);
propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueDeserializerClass);
propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
return propsMap; } /* @Bean
public Listener listener() {
return new Listener();
}*/
}
package com.dtdream.analysis.config;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory; import java.util.HashMap;
import java.util.Map; /**
* Created with IntelliJ IDEA.
* User: chenqimiao
* Date: 2017/7/24
* Time: 9:43
* To change this template use File | Settings | File Templates.
*/
@ConfigurationProperties(
prefix = "kafka.producer.default",
ignoreInvalidFields = true
)//注入一些属性域
@EnableKafka
@Configuration//使得@Bean注解生效
public class KafkaProducerConfig {
private static String server;
private static Integer retries;
private static Integer batchSize;
private static Integer lingerMs;
private static Integer bufferMemory;
private static Class keySerializerClass = StringSerializer.class;
private static Class valueSerializerClass = StringSerializer.class; private Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, server);
props.put(ProducerConfig.RETRIES_CONFIG, retries);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySerializerClass);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSerializerClass);
return props;
} private ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
} public static String getServer() {
return server;
} public static void setServer(String server) {
KafkaProducerConfig.server = server;
} public static Integer getRetries() {
return retries;
} public static void setRetries(Integer retries) {
KafkaProducerConfig.retries = retries;
} public static Integer getBatchSize() {
return batchSize;
} public static void setBatchSize(Integer batchSize) {
KafkaProducerConfig.batchSize = batchSize;
} public static Integer getLingerMs() {
return lingerMs;
} public static void setLingerMs(Integer lingerMs) {
KafkaProducerConfig.lingerMs = lingerMs;
} public static Integer getBufferMemory() {
return bufferMemory;
} public static void setBufferMemory(Integer bufferMemory) {
KafkaProducerConfig.bufferMemory = bufferMemory;
} public static Class getKeySerializerClass() {
return keySerializerClass;
} public static void setKeySerializerClass(Class keySerializerClass) {
KafkaProducerConfig.keySerializerClass = keySerializerClass;
} public static Class getValueSerializerClass() {
return valueSerializerClass;
} public static void setValueSerializerClass(Class valueSerializerClass) {
KafkaProducerConfig.valueSerializerClass = valueSerializerClass;
} @Bean(name = "kafkaTemplate")
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}

利用kafkaTemplate即可完成发送。

@Autowired
private KafkaTemplate<String,String> kafkaTemplate; @RequestMapping(
value = "/openApp",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE
)
@ResponseBody
public ResultDTO openApp(@RequestBody ActiveLogPushBo activeLogPushBo, HttpServletRequest request) { logger.info("openApp: activeLogPushBo {}, dateTime {}", JSONObject.toJSONString(activeLogPushBo),new DateTime().toString("yyyy-MM-dd HH:mm:ss.SSS")); String ip = (String) request.getAttribute("ip"); activeLogPushBo.setIp(ip); activeLogPushBo.setDate(new Date()); //ResultDTO resultDTO = dataCollectionService.collectOpenInfo(activeLogPushBo); kafkaTemplate.send("data_collection_open",JSONObject.toJSONString(activeLogPushBo)); // logger.info("openApp: resultDTO {} ,dateTime {}", resultDTO.toJSONString(),new DateTime().toString("yyyy-MM-dd HH:mm:ss.SSS")); return new ResultDTO().success();
}
kafkaTemplate的send方法会更根据你指定的key进行hash,再对partition数进行去模,最后决定发送到那一个分区,假如没有指定key,那send方法对分区的选择是随机。具体怎么随机的话,这里就不展开讲了,有兴趣的同学可以自己看源码,我们可以交流交流。

接着配置一个监听器
package com.dtdream.analysis.listener;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener; import java.util.Optional;
@Component
public class Listener { private Logger logger = LoggerFactory.getLogger(this.getClass()); @KafkaListener(topics = {"test-topic"})
public void listen(ConsumerRecord<?, ?> record) {
Optional<?> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
logger.info("message is {} ", message);
}
}
}
@KafkaListener其实可以具体指定消费哪个分区,如果不指定的话,并且只有一个消费者实例,那么这个实例会消费所有的分区的消息。

消费者的数量是一定要少于partition的数量的,不然没有任何意义。会出现消费者过剩的情况。

消费者数量和partition数量的多与少,会动态影响消费节点所消费的partition数目,最终会在整个集群中达到一种动态平衡。
具体的关系可以参考http://www.jianshu.com/p/6233d5341dfe

5.总结

理论上只要cpu核心数无限,那么partition数也可以无上限,与此同时消费者节点和生产者节点也可以无上限,最终会使单个topic的并发无上限。单机的cpu的核心数总是会达到一个上限,kafka作为分布式系统,可以很好利用集群的运算能力,进行动态扩展,在DT时代,应该会慢慢成为主流吧。

kafka生产实践的更多相关文章

  1. Redis中国用户组|唯品会Redis cluster大规模生产实践

    嘉宾:陈群 很高兴有机会在Redis中国用户组给大家分享redis cluster的生产实践.目前在唯品会主要负责redis/hbase的运维和开发支持工作,也参与工具开发工作 Outline 一.生 ...

  2. 使用Scala开发Apache Kafka的TOP 20大好用实践

    本文作者是一位软件工程师,他对20位开发人员和数据科学家使用Apache Kafka的方式进行了最大限度得深入研究,最终将生产实践环节需要注意的问题总结为本文所列的20条建议. Apache Kafk ...

  3. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

  4. 消息队列 Kafka 的基本知识及 .NET Core 客户端

    前言 最新项目中要用到消息队列来做消息的传输,之所以选着 Kafka 是因为要配合其他 java 项目中,所以就对 Kafka 了解了一下,也算是做个笔记吧. 本篇不谈论 Kafka 和其他的一些消息 ...

  5. kafka学习笔记:知识点整理

    一.为什么需要消息系统 1.解耦: 允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束. 2.冗余: 消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险. ...

  6. .net windows Kafka 安装与使用入门(入门笔记)

    完整解决方案请参考: Setting Up and Running Apache Kafka on Windows OS   在环境搭建过程中遇到两个问题,在这里先列出来,以方便查询: 1. \Jav ...

  7. kafka配置与使用实例

    kafka作为消息队列,在与netty.多线程配合使用时,可以达到高效的消息队列

  8. kafka源码分析之一server启动分析

    0. 关键概念 关键概念 Concepts Function Topic 用于划分Message的逻辑概念,一个Topic可以分布在多个Broker上. Partition 是Kafka中横向扩展和一 ...

  9. Kafka副本管理—— 为何去掉replica.lag.max.messages参数

    今天查看Kafka 0.10.0的官方文档,发现了这样一句话:Configuration parameter replica.lag.max.messages was removed. Partiti ...

随机推荐

  1. SQL Server数据类型有哪些

    一. 整数数据类型 整数数据类型是最常用的数据类型之一. 1.INT (INTEGER) INT (或INTEGER)数据类型存储从-2的31次方 (-2 ,147 ,483 ,648) 到2的31次 ...

  2. GA代码中的细节

    GA-BLX交叉-Gaussion变异 中的代码细节: 我写了一个GA的代码,在2005测试函数上一直不能得到与实验室其他同学类似的数量级的结果.现在参考其他同学的代码,发现至少有如下问题: 1.在交 ...

  3. 批量自动更新SVN版本库 - Windows

    开发过程中每天都要从SVN代码库里一个一个的update各个项目代码,不仅效率实在是低,也不符合程序员的"懒"精神,由于是在Windows环境做开发,自然就想到了使用bat来实现自 ...

  4. Visual Studio 2017 for Mac 体验之Android.Form

    微软官方说明: Visual Studio 2017 for Mac Last Update: 2017/6/16 我们非常荣幸地宣布 Visual Studio 2017 for Mac 现已推出. ...

  5. CentOS通过yum安装php7.0

    一.删除旧版本 如果已经安装过php就先删除之前的版本.检查方法如下: yum list installed | grep php 然后将安装的包进行删除 比如 yum remove php.x86_ ...

  6. stringsteam使用之整型转字符串

    最近需要用到整型转字符串的操作,学习了stringstream一些皮毛. 首先需要包含头文件. #include<sstream> 然后用流操作的方式将值传递给stringstream对象 ...

  7. 【Android Developers Training】 99. 获取联系人详细信息

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  8. 可满足性模块理论(SMT)基础 - 01 - 自动机和斯皮尔伯格算术

    可满足性模块理论(SMT)基础 - 01 - 自动机和斯皮尔伯格算术 前言 如果,我们只给出一个数学问题的(比如一道数独题)约束条件,是否有程序可以自动求出一个解? 可满足性模理论(SMT - Sat ...

  9. 【转】iOS 9 Storyboard 教程(一上)

    转自:http://blog.csdn.net/yangmeng13930719363/article/details/49886547 Storyboard是在iOS5之后新增的一个令人兴奋的功能, ...

  10. 搭建nexus私服(maven)

    这里提供nexus的直接下载页面的链接: https://www.sonatype.com/download-oss-sonatype maven获取依赖jar包是从中央仓库获取,但很莫名的出现jar ...