背景:本人做大数据开发,从消息队列kafka消费数据做数据解析、聚合,并将最终结果写入到ES存储。写ES使用到了BulkProcessor做批量写入,实际使用过程中偶发性出现ES数据重复问题

分析过程:

1、通过给每条数据打上毫秒级写入时间戳,确认是同一条数据被写入多次(2~3次),因为他们的数据内容,写入时间戳完全一致。

2、通过查看ES集群监控,确认每次ES出现数据节点CPU飙高,相应慢时会出现数据重复

3、起初怀疑是BulkProcessor的补偿机制设置,导致数据重复写,去掉重试机制后还是有数据重复问题

4、每次数据重复的次数都在3次内,最多三次,后面联想到内网连接ES集群的代理IP也是三个,推断与此有关

5、根据第4点对BulkProcessor的源码进行分析,发现导致数据重复的原因

private void performRequestAsync(RestClient.NodeTuple<Iterator<Node>> nodeTuple, RestClient.InternalRequest request, RestClient.FailureTrackingResponseListener listener) {
        request.cancellable.runIfNotCancelled(() -> {
            final RestClient.RequestContext context = request.createContextForNextAttempt((Node)((Iterator)nodeTuple.nodes).next(), nodeTuple.authCache);
            this.client.execute(context.requestProducer, context.asyncResponseConsumer, context.context, new FutureCallback<HttpResponse>() {
                public void completed(HttpResponse httpResponse) {
                    try {
                        RestClient.ResponseOrResponseException responseOrResponseException = RestClient.this.convertResponse(request, context.node, httpResponse);
                        if (responseOrResponseException.responseException == null) {
                            listener.onSuccess(responseOrResponseException.response);
                        } else if (((Iterator)nodeTuple.nodes).hasNext()) {
                            listener.trackFailure(responseOrResponseException.responseException);
                            RestClient.this.performRequestAsync(nodeTuple, request, listener);
                        } else {
                            listener.onDefinitiveFailure(responseOrResponseException.responseException);
                        }
                    } catch (Exception var3) {
                        listener.onDefinitiveFailure(var3);
                    }

                }

                public void failed(Exception failure) {
                    try {
                        RequestLogger.logFailedRequest(RestClient.logger, request.httpRequest, context.node, failure);
                        RestClient.this.onFailure(context.node);
                        if (((Iterator)nodeTuple.nodes).hasNext()) {
                            listener.trackFailure(failure);
                            RestClient.this.performRequestAsync(nodeTuple, request, listener);
                        } else {
                            listener.onDefinitiveFailure(failure);
                        }
                    } catch (Exception var3) {
                        listener.onDefinitiveFailure(var3);
                    }

                }

                public void cancelled() {
                    listener.onDefinitiveFailure(Cancellable.newCancellationException());
                }
            });
        });
    }

completed和failed方法里面都会对exception的请求做node轮询请求,这里的node数量就是client里面设置的连接IP数量,当ES出现响应超时的时候,会抛出sockettimeout exception,bulk方法就会重新先下一个节点发送写请求。而ES本身是用写队列的方式落库数据,虽然响应慢,但是实际的写请求已经进入ES,这就导致数据重复。

解决方法:目前能想到的方法,1、只能调大sockettimeout的配置;2、对ES进行性能调优,保证ES不响应超时

实际处理后效果:

1、对ES进行性能调优,读写的并发度和batch做了调优,刚开始有效果,后面索引量变大,数据量变大已经出现响应超时问题

2、调大sockettimeout效应时间出现写入慢的情况,反过来阻塞上游数据消费,导致了消费堆积,出现生产事故

3、后面在VIP层面开启单ip负载均衡发送请求到client节点的操作,从根源解决突刺问题