生成者API在不同版本间无较大变动;消费者(Consumer)提供了两套API
两种API的优缺点:
高版本Consumer API 优点:
高版本Consumer API 缺点:
低版本Consumer API 优点:
低版本Consumer API 缺点:
使用者可以根据自身情对两种API况进行选取。
Kafka的Producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main线程和Sender线程,以及一个线程共享变量——RecordAccumulator。main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。
注意事项:参数说明
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-clientsartifactId>
<version>0.11.0.0version>
dependency>
常用的类
KafkaProducer 的构造方法
public KafkaProducer(Map<String, Object> configs) {
this(new ProducerConfig(configs), null, null);
}
public KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
this(new ProducerConfig(ProducerConfig.addSerializerToConfig(configs, keySerializer, valueSerializer)),
keySerializer, valueSerializer);
}
public KafkaProducer(Properties properties) {
this(new ProducerConfig(properties), null, null);
}
private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
try {
log.trace("Starting the Kafka producer");
Map<String, Object> userProvidedConfigs = config.originals();
this.producerConfig = config;
......
}
上述源码可以看出,构造ProducerConfig来完成KafkaProducer的创建。而ProducerConfig的构造可以通过Map或者Properties来完成。本次代码示例使用Properties完成ProducerConfig的创建。
属性 | 描述 | 类型 | 默认值 |
---|---|---|---|
bootstrap.servers | 用于建立与kafka集群的连接,这个list仅仅影响用于初始化的hosts,来发现全部的servers。 格式:host1:port1,host2:port2,…,数量尽量不止一个,以防其中一个down了 |
list | |
acks | Server完成 producer request 前需要确认的数量。acks=0 时,producer不会等待确认,直接添加到socket等待发送;acks=1 时,等待leader写到local log就行;acks=all 或acks=-1 时,等待isr中所有副本确认(注意:确认都是 broker 接收到消息放入内存就直接返回确认,不是需要等待数据写入磁盘后才返回确认,这也是kafka快的原因) |
string | 1 |
buffer.memory | Producer可以用来缓存数据的内存大小。该值实际为RecordAccumulator类中的BufferPool,即Producer所管理的最大内存。 如果数据产生速度大于向broker发送的速度,producer会阻塞 max.block.ms ,超时则抛出异常 |
long | 33554432 |
compression.type | Producer用于压缩数据的压缩类型,取值:none, gzip, snappy, or lz4 |
string | none |
batch.size | Producer可以将发往同一个Partition的数据做成一个Produce Request发送请求,即Batch批处理,以减少请求次数,该值即为每次批处理的大小。 另外每个Request请求包含多个Batch,每个Batch对应一个Partition,且一个Request发送的目的Broker均为这些partition的leader副本。 若将该值设为0,则不会进行批处理 |
int | 16384 |
linger.ms | Producer默认会把两次发送时间间隔内收集到的所有Requests进行一次聚合然后再发送,以此提高吞吐量,而linger.ms则更进一步,这个参数为每次发送增加一些delay,以此来聚合更多的Message。 官网解释翻译:producer会将request传输之间到达的所有records聚合到一个批请求。通常这个值发生在欠负载情况下,record到达速度快于发送。但是在某些场景下,client即使在正常负载下也期望减少请求数量。这个设置就是如此,通过人工添加少量时延,而不是立马发送一个record,producer会等待所给的时延,以让其他records发送出去,这样就会被聚合在一起。这个类似于TCP的Nagle算法。该设置给了batch的时延上限:当我们获得一个partition的 batch.size 大小的records,就会立即发送出去,而不管该设置;但是如果对于这个partition没有累积到足够的record,会linger 指定的时间等待更多的records出现。该设置的默认值为0(无时延)。例如,设置linger.ms=5 ,会减少request发送的数量,但是在无负载下会增加5ms的发送时延。 |
long | 0 |
max.request.size | 请求的最大字节数。这也是对最大消息大小的有效限制。注意:server具有自己对消息大小的限制,这些大小和这个设置不同。此项设置将会限制producer每次批量发送请求的数目,以防发出巨量的请求。 | int | 1048576 |
receive.buffer.bytes | TCP的接收缓存 SO_RCVBUF 空间大小,用于读取数据 | int | 32768 |
request.timeout.ms | client等待请求响应的最大时间,如果在这个时间内没有收到响应,客户端将重发请求,超过重试次数发送失败 | int | 30000 |
send.buffer.bytes | TCP的发送缓存 SO_SNDBUF 空间大小,用于发送数据 | int | 131072 |
timeout.ms | 指定server等待来自followers的确认的最大时间,根据acks 的设置,超时则返回error |
int | 30000 |
max.in.flight.requests.per.connection | 在block前一个connection上允许最大未确认的requests数量。 当设为1时,即是消息保证有序模式,注意:这里的消息保证有序是指对于单个Partition的消息有顺序,因此若要保证全局消息有序,可以只使用一个Partition,当然也会降低性能 |
int | 5 |
metadata.fetch.timeout.ms | 在第一次将数据发送到某topic时,需先fetch该topic的metadata,得知哪些服务器持有该topic的partition,该值为最长获取metadata时间 | long | 60000 |
reconnect.backoff.ms | 连接失败时,当我们重新连接时的等待时间 | long | 50 |
retry.backoff.ms | 在重试发送失败的request前的等待时间,防止若目的Broker完全挂掉的情况下Producer一直陷入死循环发送,折中的方法 | long | 100 |
概括下代码过程的大致过程:
1. 生产者通过KafkaProducer进行封装,通过该对象进行消息的生产。
2. KafkaProducer对象需要进行一些配置,而配置的创建依赖于ProducerConfig。
3. KafkaProducer生产的消息的载体为ProducerRecord。
有无回调函数的原因是kafkaProducer.send()方法重载。
public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
return send(record, null);
}
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
return doSend(interceptedRecord, callback);
}
Properties properties = new Properties();
// bootstrap.servers 配置kafka集群中broker-list
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.115.157:9092");
// 配置消息确认(所有副本均确认后,返回确认消息)
properties.put(ProducerConfig.ACKS_CONFIG, "1");
// 重试次数
properties.put("retries", 1);
// 批次大小
properties.put("batch.size", 16384);
// 等待时间
properties.put("linger.ms", 1);
// RecordAccumulator缓冲区大小
properties.put("buffer.memory", 33554432);
// 设置生产者生产的消息的key值的数据类型
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 设置生产者生产的消息的value值的数据类型
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 构建KafkaProducer对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 模拟生产100消息
for (int i = 0; i < 100; i++) {
kafkaProducer.send(new ProducerRecord<String, String>("test", "key" + i, "value" + i ));
// 若想对返回值进行处理则可以加上Callback参数
// kafkaProducer.send(new ProducerRecord("test", "key" + i, "value" + i), (metadata, exception) -> {
// if (exception == null){
// // 无异常,输出偏移量
// System.out.println(metadata.offset());
// } else {
// exception.printStackTrace();
// }
// });
}
// 关闭生产者
kafkaProducer.close();
同步发送的特点是,一条消息发送后,会阻塞当前线程,直至返回ack为止。
利用又返回值的KafkaProducer的send方法,返回的Future对象来实现同步发送的效果。
即kafkaProducer.send().get();
get();方法是获取返回对象,而该方法会阻塞主线程,直至获取到返回对象为止。
......
for (int i = 0; i < 100; i++) {
RecordMetadata test = kafkaProducer.send(new ProducerRecord<String, String>("test", "key" + i, "value" + i), (metadata, exception) -> {
if (exception == null) {
// 无异常,输出偏移量
System.out.println(metadata.offset());
} else {
exception.printStackTrace();
}
}).get();
System.out.println(test.offset());
}
消费者业务逻辑的关键在于维护offset;
两种方式
与Producer相似,有以下常用类
NAME | DESCRIPTION | TYPE | DEFAULT |
---|---|---|---|
bootstrap.servers | host/port,用于和kafka集群建立初始化连接。因为这些服务器地址仅用于初始化连接,并通过现有配置的来发现全部的kafka集群成员(集群随时会变化),所以此列表不需要包含完整的集群地址(但尽量多配置几个,以防止配置的服务器宕机)。 | list | |
key.deserializer | key的解析序列化接口实现类(Deserializer)。 | class | |
value.deserializer | value的解析序列化接口实现类(Deserializer) | class | |
fetch.min.bytes | 服务器哦拉取请求返回的最小数据量,如果数据不足,请求将等待数据积累。默认设置为1字节,表示只要单个字节的数据可用或者读取等待请求超时,就会应答读取请求。将此值设置的越大将导致服务器等待数据累积的越长,这可能以一些额外延迟为代价提高服务器吞吐量。 | int | 1 |
group.id | 此消费者所属消费者组的唯一标识。如果消费者用于订阅或offset管理策略的组管理功能,则此属性是必须的。 | string | "" |
heartbeat.interval.ms | 当使用Kafka的分组管理功能时,心跳到消费者协调器之间的预计时间。心跳用于确保消费者的会话保持活动状态,并当有新消费者加入或离开组时方便重新平衡。该值必须必比session.timeout.ms小,通常不高于1/3。它可以调整的更低,以控制正常重新平衡的预期时间。 | int | 3000 |
max.partition.fetch.bytes | 服务器将返回每个分区的最大数据量。如果拉取的第一个非空分区中第一个消息大于此限制,则仍然会返回消息,以确保消费者可以正常的工作。broker接受的最大消息大小通过message.max.bytes (broker config)或max.message.bytes (topic config)定义。参阅fetch.max.bytes以限制消费者请求大小。 |
int | 1048576 |
session.timeout.ms | 用于发现消费者故障的超时时间。消费者周期性的发送心跳到broker,表示其还活着。如果会话超时期满之前没有收到心跳,那么broker将从分组中移除消费者,并启动重新平衡。请注意,该值必须在broker配置的group.min.session.timeout.ms 和group.max.session.timeout.ms 允许的范围内。 |
int | 10000 |
ssl.key.password | 密钥存储文件中的私钥的密码。 客户端可选 | password | null |
ssl.keystore.location | 密钥存储文件的位置, 这对于客户端是可选的,并且可以用于客户端的双向认证。 | string | null |
ssl.keystore.password | 密钥仓库文件的仓库密码。客户端可选,只有ssl.keystore.location配置了才需要。 | password | null |
ssl.truststore.location | 信任仓库文件的位置 | string | null |
ssl.truststore.password | 信任仓库文件的密码 | password | null |
auto.offset.reset | 当Kafka中没有初始offset或如果当前的offset不存在时(例如,该数据被删除了),该怎么办。 最早:自动将偏移重置为最早的偏移 最新:自动将偏移重置为最新偏移 none:如果消费者组找到之前的offset,则向消费者抛出异常 其他:抛出异常给消费者。 |
string | latest |
connections.max.idle.ms | 指定在多少毫秒之后关闭闲置的连接 | long | 540000 |
enable.auto.commit | 如果为true,消费者的offset将在后台周期性的提交 | boolean | true |
exclude.internal.topics | 内部topic的记录(如偏移量)是否应向消费者公开。如果设置为true,则从内部topic接受记录的唯一方法是订阅它。 | boolean | true |
fetch.max.bytes | 服务器为拉取请求返回的最大数据值。这不是绝对的最大值,如果在第一次非空分区拉取的第一条消息大于该值,该消息将仍然返回,以确保消费者继续工作。接收的最大消息大小通过message.max.bytes (broker config) 或 max.message.bytes (topic config)定义。注意,消费者是并行执行多个提取的。 | int | 52428800 |
max.poll.interval.ms | 使用消费者组管理时poll()调用之间的最大延迟。消费者在获取更多记录之前可以空闲的时间量的上限。如果此超时时间期满之前poll()没有调用,则消费者被视为失败,并且分组将重新平衡,以便将分区重新分配给别的成员。 | int | 300000 |
max.poll.records | 在单次调用poll() 中返回的最大记录数。 |
int | 500 |
partition.assignment.strategy | 当使用组管理时,客户端将使用分区分配策略的类名来分配消费者实例之间的分区所有权 | list | class org.apache.kafka .clients.consumer .RangeAssignor |
receive.buffer.bytes | 读取数据时使用的TCP接收缓冲区(SO_RCVBUF)的大小。 如果值为-1,则将使用OS默认值。 | int | 65536 |
request.timeout.ms | 配置控制客户端等待请求响应的最长时间。 如果在超时之前未收到响应,客户端将在必要时重新发送请求,如果重试耗尽则客户端将重新发送请求。 | int | 305000 |
sasl.jaas.config | JAAS配置文件中SASL连接登录上下文参数。 这里描述JAAS配置文件格式。 该值的格式为: '(=)*;' | password | null |
sasl.kerberos.service.name | Kafka运行Kerberos principal名。可以在Kafka的JAAS配置文件或在Kafka的配置文件中定义。 | string | null |
sasl.mechanism | 用于客户端连接的SASL机制。安全提供者可用的机制。GSSAPI是默认机制。 | string | GSSAPI |
security.protocol | 用于与broker通讯的协议。 有效值为:PLAINTEXT,SSL,SASL_PLAINTEXT,SASL_SSL。 | string | PLAINTEXT |
send.buffer.bytes | 发送数据时要使用的TCP发送缓冲区(SO_SNDBUF)的大小。 如果值为-1,则将使用OS默认值。 | int | 131072 |
ssl.enabled.protocols | 启用SSL连接的协议列表。 | list | TLSv1.2,TLSv1.1,TLSv1 |
ssl.keystore.type | key仓库文件的文件格式,客户端可选。 | string | JKS |
ssl.protocol | 用于生成SSLContext的SSL协议。 默认设置是TLS,这对大多数情况都是适用的。 最新的JVM中的允许值为TLS,TLSv1.1和TLSv1.2。 较旧的JVM可能支持SSL,SSLv2和SSLv3,但由于已知的安全漏洞,不建议使用SSL。 | string | TLS |
ssl.provider | 用于SSL连接的安全提供程序的名称。 默认值是JVM的默认安全提供程序。 | string | null |
ssl.truststore.type | 信任存储文件的文件格式。 | string | JKS |
auto.commit.interval.ms | 如果enable.auto.commit设置为true,则消费者偏移量自动提交给Kafka的频率(以毫秒为单位)。 | int | 5000 |
check.crcs | 自动检查CRC32记录的消耗。 这样可以确保消息发生时不会在线或磁盘损坏。 此检查增加了一些开销,因此在寻求极致性能的情况下可能会被禁用。 | boolean | true |
client.id | 在发出请求时传递给服务器的id字符串。 这样做的目的是通过允许将逻辑应用程序名称包含在服务器端请求日志记录中,来跟踪ip/port的请求源。 | string | "" |
fetch.max.wait.ms | 如果没有足够的数据满足fetch.min.bytes,服务器将在接收到提取请求之前阻止的最大时间。 | int | 500 |
interceptor.classes | 用作拦截器的类的列表。 你可实现ConsumerInterceptor接口以允许拦截(也可能变化)消费者接收的记录。 默认情况下,没有拦截器。 | list | null |
metadata.max.age.ms | 在一定时间段之后(以毫秒为单位的),强制更新元数据,即使没有任何分区领导变化,任何新的broker或分区。 | long | 300000 |
metric.reporters | 用作度量记录员类的列表。实现MetricReporter接口以允许插入通知新的度量创建的类。JmxReporter始终包含在注册JMX统计信息中。 | list | "" |
metrics.num.samples | 保持的样本数以计算度量。 | int | 2 |
metrics.recording.level | 最高的记录级别。 | string | INFO |
metrics.sample.window.ms | The window of time a metrics sample is computed over. | long | 30000 |
reconnect.backoff.ms | 尝试重新连接指定主机之前等待的时间,避免频繁的连接主机,这种机制适用于消费者向broker发送的所有请求。 | long | 50 |
retry.backoff.ms | 尝试重新发送失败的请求到指定topic分区之前的等待时间。避免在某些故障情况下,频繁的重复发送。 | long | 100 |
sasl.kerberos.kinit.cmd Kerberos | kinit命令路径。 | string | /usr/bin/kinit |
sasl.kerberos.min.time.before.relogin | 尝试/恢复之间的登录线程的休眠时间。 | long | 60000 |
sasl.kerberos.ticket.renew.jitter | 添加到更新时间的随机抖动百分比。 | double | 0.05 |
sasl.kerberos.ticket.renew.window.factor | 登录线程将休眠,直到从上次刷新到ticket的指定的时间窗口因子到期,此时将尝试续订ticket。 | double | 0.8 |
ssl.cipher.suites | 密码套件列表,用于TLS或SSL网络协议的安全设置,认证,加密,MAC和密钥交换算法的明明组合。默认情况下,支持所有可用的密码套件。 | list | null |
ssl.endpoint.identification.algorithm | 使用服务器证书验证服务器主机名的端点识别算法。 | string | null |
ssl.keymanager.algorithm | 密钥管理器工厂用于SSL连接的算法。 默认值是为Java虚拟机配置的密钥管理器工厂算法。 | string | SunX509 |
ssl.secure.random.implementation | 用于SSL加密操作的SecureRandom PRNG实现。 | string | null |
ssl.trustmanager.algorithm | 信任管理器工厂用于SSL连接的算法。 默认值是为Java虚拟机配置的信任管理器工厂算法。 | string | PKIX |
从上表提取出一些项目中常用的配置
fetch.min.bytes
这个参数允许消费者指定从broker读取消息时最小的数据量。当消费者从broker读取消息时,如果数据量小于这个阈值,broker会等待直到有足够的数据,然后才返回给消费者。对于写入量不高的主题来说,这个参数可以减少broker和消费者的压力,因为减少了往返的时间。而对于有大量消费者的主题来说,则可以明显减轻broker压力。
fetch.max.wait.ms
上面的fetch.min.bytes参数指定了消费者读取的最小数据量,而这个参数则指定了消费者读取时最长等待时间,从而避免长时间阻塞。这个参数默认为500ms。
max.partition.fetch.bytes
这个参数指定了每个分区返回的最多字节数,默认为1M。也就是说,KafkaConsumer.poll()返回记录列表时,每个分区的记录字节数最多为1M。如果一个主题有20个分区,同时有5个消费者,那么每个消费者需要4M的空间来处理消息。实际情况中,我们需要设置更多的空间,这样当存在消费者宕机时,其他消费者可以承担更多的分区。
需要注意的是,max.partition.fetch.bytes必须要比broker能够接收的最大的消息(由max.message.size设置)大,否则会导致消费者消费不了消息。另外,在上面的样例可以看到,我们通常循环调用poll方法来读取消息,如果max.partition.fetch.bytes设置过大,那么消费者需要更长的时间来处理,可能会导致没有及时poll而会话过期。对于这种情况,要么减小max.partition.fetch.bytes,要么加长会话时间。
session.timeout.ms
这个参数设置消费者会话过期时间,默认为3秒。也就是说,如果消费者在这段时间内没有发送心跳,那么broker将会认为会话过期而进行分区重平衡。这个参数与heartbeat.interval.ms有关,heartbeat.interval.ms控制KafkaConsumer的poll()方法多长时间发送一次心跳,这个值需要比session.timeout.ms小,一般为1/3,也就是1秒。更小的session.timeout.ms可以让Kafka快速发现故障进行重平衡,但也加大了误判的概率(比如消费者可能只是处理消息慢了而不是宕机)。
auto.offset.reset
这个参数指定了当消费者第一次读取分区或者上一次的位置太老(比如消费者下线时间太久)时的行为,可以取值为latest(从最新的消息开始消费)或者earliest(从最老的消息开始消费)。
enable.auto.commit
这个参数指定了消费者是否自动提交消费位移,默认为true。如果需要减少重复消费或者数据丢失,你可以设置为false。如果为true,你可能需要关注自动提交的时间间隔,该间隔由auto.commit.interval.ms设置。
partition.assignment.strategy
我们已经知道当消费组存在多个消费者时,主题的分区需要按照一定策略分配给消费者。这个策略由PartitionAssignor类决定,默认有两种策略:
client.id
这个参数可以为任意值,用来指明消息从哪个客户端发出,一般会在打印日志、衡量指标、分配配额时使用。
max.poll.records
这个参数控制一个poll()调用返回的记录数,这个可以用来控制应用在拉取循环中的处理数据量。
receive.buffer.bytes、send.buffer.bytes
这两个参数控制读写数据时的TCP缓冲区,设置为-1则使用系统的默认值。如果消费者与broker在不同的数据中心,可以一定程度加大缓冲区,因为数据中心间一般的延迟都比较大。
大致过程
自动提交和手动提交
简单的kafka消费者代码示例
public static void main(String[] args) {
Properties properties = new Properties();
// 配置kafka broker-list
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.115.157:9092");
// 设置消费者消费的消息的key值的数据类型
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 设置消费者消费的消息的value值的数据类型
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 配置消费者组,相同groupId为同一个消费组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "group_test");
// 自动提交offset关闭
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
// 下次消费的位置
// 可以取值为latest(从最新的消息开始消费)或者earliest(从最老的消息开始消费)。
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// 创建KafkaConsumer对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 订阅消费的Topic - Topic名为test
kafkaConsumer.subscribe(Arrays.asList("test"));
// 消费数据
try {
while (true) {
// 每100毫秒消费一次消息
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(100);
consumerRecords.forEach(consumerRecord -> {
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n",
consumerRecord.topic(), consumerRecord.partition(), consumerRecord.offset(),
consumerRecord.key(), consumerRecord.value());
});
// 异步提交offset
kafkaConsumer.commitSync();
}
} catch(CommitFailedException e){
e.printStackTrace();
} finally {
// 关闭消费者
kafkaConsumer.close();
}
}
自动提交
异步提交与同步提交
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。
获取分区定位以及重新分区
在消费者订阅主题中有个重载方法可以定义获取的分区以及重新分区
// 订阅消费的Topic - Topic名为test
kafkaConsumer.subscribe(Arrays.asList("test"),
new ConsumerRebalanceListener() {
// 定义了重新获取分区(消费者组中新增了消费者后,消费者对应的分区会根据分区规则进行重新分区)
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("====重新分区====");
partitions.forEach(partition->{
System.out.printf("Topic = %s , partition = %i", partition.topic(), partition.partition());
});
}
// 定位新分配分区的offset
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
System.out.println("====初次获取分区====");
partitions.forEach(partition->{
System.out.printf("Topic = %s , partition = %i", partition.topic(), partition.partition());
// 获取offset (伪代码,从ZK或者存储offset的位置中获取对应TopicPartition的offset)
long offset = getoffset(partition);
// 定位offset,后续消费从offset后消费消息
kafkaConsumer.seek(partition, offset + 1);
});
}
}
);