You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@inlong.apache.org by al...@apache.org on 2021/10/22 04:43:27 UTC

[incubator-inlong] branch master updated: [INLONG-1330][DataProxy] Support pulsar sink (#1705)

This is an automated email from the ASF dual-hosted git repository.

aloyszhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-inlong.git


The following commit(s) were added to refs/heads/master by this push:
     new 219370f  [INLONG-1330][DataProxy] Support pulsar sink (#1705)
219370f is described below

commit 219370fd031107f0ab08b31cff7ac55e8f43b58f
Author: baomingyu <ba...@163.com>
AuthorDate: Fri Oct 22 12:43:19 2021 +0800

    [INLONG-1330][DataProxy] Support pulsar sink (#1705)
    
    * add pulsar sink feature
    
    * support mult pulsar cluster
    
    * modify check style
    
    * modify check style
    
    * modify check style
---
 inlong-dataproxy/dataproxy-source/pom.xml          |   6 +
 .../dataproxy/base/HighPriorityThreadFactory.java  |  53 ++
 .../apache/inlong/dataproxy/sink/PulsarSink.java   | 778 +++++++++++++++++++++
 .../sink/pulsar/CreatePulsarClientCallBack.java    |  25 +
 .../dataproxy/sink/pulsar/PulsarClientService.java | 347 +++++++++
 .../dataproxy/sink/pulsar/SendMessageCallBack.java |  27 +
 .../inlong/dataproxy/source/SimpleTcpSource.java   |   2 +-
 .../utils/FailoverChannelProcessorHolder.java      |  32 +
 .../apache/inlong/dataproxy/utils/LogCounter.java  |  50 ++
 .../inlong/dataproxy/utils/MonitorIndex.java       | 127 ++++
 .../inlong/dataproxy/utils/MonitorIndexExt.java    | 125 ++++
 .../dataproxy-source/src/main/resources/flume.conf |  10 +
 .../inlong/dataproxy/sink/TestPulsarSink.java      |  96 +++
 inlong-dataproxy/pom.xml                           |  13 +
 14 files changed, 1690 insertions(+), 1 deletion(-)

diff --git a/inlong-dataproxy/dataproxy-source/pom.xml b/inlong-dataproxy/dataproxy-source/pom.xml
index 301a4fb..b973a20 100644
--- a/inlong-dataproxy/dataproxy-source/pom.xml
+++ b/inlong-dataproxy/dataproxy-source/pom.xml
@@ -33,6 +33,7 @@
     <properties>
         <maven.compiler.source>8</maven.compiler.source>
         <maven.compiler.target>8</maven.compiler.target>
+        <guava.version>19.0</guava.version>
     </properties>
 
     <dependencies>
@@ -42,6 +43,11 @@
             <version>${project.version}</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </dependency>
     </dependencies>
 
 </project>
\ No newline at end of file
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/base/HighPriorityThreadFactory.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/base/HighPriorityThreadFactory.java
new file mode 100644
index 0000000..07557fc
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/base/HighPriorityThreadFactory.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.base;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class HighPriorityThreadFactory implements ThreadFactory {
+    static final AtomicInteger poolNumber = new AtomicInteger(1);
+    final AtomicInteger threadNumber;
+    final ThreadGroup group;
+    final String namePrefix;
+    final boolean isDaemon;
+
+    public HighPriorityThreadFactory() {
+        this("pool");
+    }
+
+    public HighPriorityThreadFactory(String name) {
+        this(name, false);
+    }
+
+    public HighPriorityThreadFactory(String prefix, boolean daemon) {
+        this.threadNumber = new AtomicInteger(1);
+        SecurityManager s = System.getSecurityManager();
+        this.group = s != null ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
+        this.namePrefix = prefix + "-thread-" + poolNumber.getAndIncrement();
+        this.isDaemon = daemon;
+    }
+
+    public Thread newThread(Runnable r) {
+        Thread t = new Thread(this.group, r, this.namePrefix + this.threadNumber.getAndIncrement(), 0L);
+        t.setDaemon(this.isDaemon);
+        t.setPriority(10);
+        return t;
+    }
+}
+
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/PulsarSink.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/PulsarSink.java
new file mode 100644
index 0000000..2a180e8
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/PulsarSink.java
@@ -0,0 +1,778 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.sink;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.RateLimiter;
+import org.apache.inlong.dataproxy.base.HighPriorityThreadFactory;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.flume.Channel;
+import org.apache.flume.Context;
+import org.apache.flume.Event;
+import org.apache.flume.EventDeliveryException;
+import org.apache.flume.Transaction;
+import org.apache.flume.conf.Configurable;
+
+import org.apache.flume.instrumentation.SinkCounter;
+import org.apache.flume.sink.AbstractSink;
+import org.apache.inlong.dataproxy.consts.AttributeConstants;
+import org.apache.inlong.dataproxy.consts.ConfigConstants;
+import org.apache.inlong.dataproxy.config.ConfigManager;
+import org.apache.inlong.dataproxy.config.holder.ConfigUpdateCallback;
+import org.apache.inlong.dataproxy.sink.pulsar.CreatePulsarClientCallBack;
+import org.apache.inlong.dataproxy.sink.pulsar.PulsarClientService;
+import org.apache.inlong.dataproxy.sink.pulsar.SendMessageCallBack;
+import org.apache.inlong.dataproxy.utils.FailoverChannelProcessorHolder;
+import org.apache.inlong.dataproxy.utils.LogCounter;
+import org.apache.inlong.dataproxy.utils.MonitorIndex;
+import org.apache.inlong.dataproxy.utils.MonitorIndexExt;
+import org.apache.inlong.dataproxy.utils.NetworkUtils;
+import org.apache.pulsar.client.api.PulsarClientException;
+import org.apache.pulsar.client.api.PulsarClientException.AlreadyClosedException;
+import org.apache.pulsar.client.api.PulsarClientException.NotConnectedException;
+import org.apache.pulsar.client.api.PulsarClientException.ProducerQueueIsFullError;
+import org.apache.pulsar.client.api.PulsarClientException.TopicTerminatedException;
+import org.jboss.netty.handler.codec.frame.TooLongFrameException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * use pulsarSink need adding such config, if these ara not config in flume.conf, PulsarSink will
+ * use default value.
+ * prefix of pulsar sink config in flume.conf like this XXX.sinks.XXX.property
+ * and properties are may these Configurations:
+ *  type (*): value must be 'org.apache.inlong.dataproxy.sink.PulsarSink'
+ *  pulsar_server_url_list (*): value is pulsar broker url , like this 'pulsar://127.0.0.1:6650'
+ *  send_timeout_MILL: send message timeout, unit is millisecond, default value is 30000 (mean 30s)
+ *  stat_interval_sec: stat info will be made period time , unit is second, default value is 60s
+ *  thread-num: sink thread num. default value  is 8
+ *  client-id-cache: whether use cache in client, default value is true
+ *  max_pending_messages: default value is 10000
+ *  max_batching_messages: default value is 1000
+ *  enable_batch: default is true
+ *  block_if_queue_full: default is true
+ */
+public class PulsarSink extends AbstractSink implements Configurable,
+        SendMessageCallBack, CreatePulsarClientCallBack {
+
+    private static final Logger logger = LoggerFactory.getLogger(PulsarSink.class);
+
+    /*
+     * properties for header info
+     */
+    private static String TOPIC = "topic";
+
+    /*
+     * default value
+     */
+    private static int BAD_EVENT_QUEUE_SIZE = 10000;
+    private static int BATCH_SIZE = 10000;
+    private static final int DEFAULT_RETRY_CNT = -1;
+    private static final int DEFAULT_LOG_EVERY_N_EVENTS = 100000;
+    private static final int DEFAULT_STAT_INTERVAL_SEC = 60;
+
+    /*
+     * properties for stat
+     */
+    private static String LOG_EVERY_N_EVENTS = "log-every-n-events";
+
+    private static String CLIENT_ID_CACHE = "client_id_cache";
+
+    private static String RETRY_CNT = "retry_currentSuccSendedCnt";
+
+    private static String STAT_INTERVAL_SEC = "stat_interval_sec";
+    /*
+     * for log
+     */
+    private Integer logEveryNEvents;
+    private long diskIORatePerSec;
+    private RateLimiter diskRateLimiter;
+
+    /*
+     * for stat
+     */
+    private AtomicLong currentSuccessSendCnt = new AtomicLong(0);
+    private AtomicLong lastSuccessSendCnt = new AtomicLong(0);
+    private long t1 = System.currentTimeMillis();
+    private long t2 = 0L;
+    private static AtomicLong totalPulsarSuccSendCnt = new AtomicLong(0);
+    private static AtomicLong totalPulsarSuccSendSize = new AtomicLong(0);
+    /*
+     * for control
+     */
+    private static int retryCnt = DEFAULT_RETRY_CNT;
+    private boolean overflow = false;
+
+    private LinkedBlockingQueue<EventStat> resendQueue;
+
+    private int maxMonitorCnt = 300000;
+
+    private int statIntervalSec = DEFAULT_STAT_INTERVAL_SEC;
+    private long logCounter = 0;
+
+    private final AtomicLong currentInFlightCount = new AtomicLong(0);
+
+    private boolean clientIdCache = false;
+
+    private static ConcurrentHashMap<String, Long> illegalTopicMap =
+            new ConcurrentHashMap<String, Long>();
+
+    /*
+     * whether the SendTask thread can send data to pulsar
+     */
+    private volatile boolean canSend = false;
+
+    /*
+     * Control whether the SinkRunner thread can read data from the Channel
+     */
+    private volatile boolean canTake = false;
+
+    /*
+     * log tools
+     */
+    private static final LogCounter logPrinterA = new LogCounter(10, 100000, 60 * 1000);
+    private static final LogCounter logPrinterB = new LogCounter(10, 100000, 60 * 1000);
+    private static final LogCounter logPrinterC = new LogCounter(10, 100000, 60 * 1000);
+
+
+    private static final String SINK_THREAD_NUM = "thread-num";
+    private static int EVENT_QUEUE_SIZE = 1000;
+    private int threadNum;
+
+
+    /*
+     * send thread pool
+     */
+    private Thread[] sinkThreadPool;
+    private LinkedBlockingQueue<Event> eventQueue;
+
+    private static final String SEPARATOR = "#";
+    private boolean isNewMetricOn = true;
+    private MonitorIndex monitorIndex;
+    private MonitorIndexExt monitorIndexExt;
+    private SinkCounter sinkCounter;
+    private ConfigManager configManager;
+    private Map<String, String> topicProperties;
+
+    private PulsarClientService pulsarClientService;
+
+    private static final Long PRINT_INTERVAL = 30L;
+
+    private static final PulsarPerformanceTask pulsarPerformanceTask = new PulsarPerformanceTask();
+
+    private static ScheduledExecutorService scheduledExecutorService = Executors
+            .newScheduledThreadPool(1, new HighPriorityThreadFactory("pulsarPerformance-Printer-thread"));
+
+    private static final  LoadingCache<String, Long> agentIdCache = CacheBuilder.newBuilder()
+            .concurrencyLevel(4 * 8).initialCapacity(500).expireAfterAccess(30, TimeUnit.SECONDS)
+            .build(new CacheLoader<String, Long>() {
+                //默认的数据加载实现,当调用get取值时,如果key没有对应的值,则调用此方法进行加载。
+                @Override
+                public Long load(String key) {
+                    return System.currentTimeMillis();
+                }
+            });
+
+    static {
+        /*
+         * stat pulsar performance
+         */
+        System.out.println("pulsarPerformanceTask!!!!!!");
+        scheduledExecutorService.scheduleWithFixedDelay(pulsarPerformanceTask, 0L,
+                PRINT_INTERVAL, TimeUnit.SECONDS);
+    }
+
+    public PulsarSink() {
+        super();
+        logger.debug("new instance of PulsarSink!");
+    }
+
+    public void configure(Context context) {
+        logger.info("PulsarSink started and context = {}", context.toString());
+        isNewMetricOn = context.getBoolean("new-metric-on", true);
+        maxMonitorCnt = context.getInteger("max-monitor-cnt",300000);
+
+        configManager = ConfigManager.getInstance();
+        topicProperties = configManager.getTopicProperties();
+        configManager.getTopicConfig().addUpdateCallback(new ConfigUpdateCallback() {
+            @Override
+            public void update() {
+                diffSetPublish(new HashSet<String>(topicProperties.values()),
+                        new HashSet<String>(configManager.getTopicProperties().values()));
+            }
+        });
+
+        retryCnt = context.getInteger(RETRY_CNT, DEFAULT_RETRY_CNT);
+        logger.debug(this.getName() + " " + RETRY_CNT + " " + retryCnt);
+
+        statIntervalSec = context.getInteger(STAT_INTERVAL_SEC, DEFAULT_STAT_INTERVAL_SEC);
+        logger.debug(this.getName() + " " + STAT_INTERVAL_SEC + " " + statIntervalSec);
+
+        logEveryNEvents = context.getInteger(LOG_EVERY_N_EVENTS, DEFAULT_LOG_EVERY_N_EVENTS);
+        logger.debug(this.getName() + " " + LOG_EVERY_N_EVENTS + " " + logEveryNEvents);
+        Preconditions.checkArgument(logEveryNEvents > 0, "logEveryNEvents must be > 0");
+        Preconditions.checkArgument(statIntervalSec >= 0, "statIntervalSec must be >= 0");
+
+        clientIdCache = context.getBoolean(CLIENT_ID_CACHE, clientIdCache);
+
+        resendQueue = new LinkedBlockingQueue<EventStat>(BAD_EVENT_QUEUE_SIZE);
+
+        String sinkThreadNum = context.getString(SINK_THREAD_NUM, "4");
+        threadNum = Integer.parseInt(sinkThreadNum);
+        Preconditions.checkArgument(threadNum > 0, "threadNum must be > 0");
+        sinkThreadPool = new Thread[threadNum];
+        eventQueue = new LinkedBlockingQueue<Event>(EVENT_QUEUE_SIZE);
+
+        diskIORatePerSec = context.getLong("disk-io-rate-per-sec",0L);
+        if (diskIORatePerSec != 0) {
+            diskRateLimiter = RateLimiter.create(diskIORatePerSec);
+        }
+        pulsarClientService = new PulsarClientService(context);
+
+        if (sinkCounter == null) {
+            sinkCounter = new SinkCounter(getName());
+        }
+    }
+
+    private void initTopicSet(Set<String> topicSet) throws Exception {
+        long startTime = System.currentTimeMillis();
+        if (topicSet != null) {
+            for (String topic : topicSet) {
+                pulsarClientService.initTopicProducer(topic);
+            }
+        }
+        logger.info(getName() + " initTopicSet cost: "
+                + (System.currentTimeMillis() - startTime) + "ms");
+        logger.info(getName() + " producer is ready for topics : "
+                + pulsarClientService.getProducerInfoMap().keySet());
+    }
+
+    /**
+     * When topic.properties is re-enabled, the producer update is triggered
+     * @param originalSet
+     * @param endSet
+     */
+    public void diffSetPublish(Set<String> originalSet, Set<String> endSet) {
+
+        boolean changed = false;
+        for (String s : endSet) {
+            if (!originalSet.contains(s)) {
+                changed = true;
+                try {
+                    pulsarClientService.initTopicProducer(s);
+                } catch (Exception e) {
+                    logger.error("Get producer failed!", e);
+                }
+            }
+        }
+        if (changed) {
+            logger.info("topics.properties has changed, trigger diff publish for {}", getName());
+            topicProperties = configManager.getTopicProperties();
+        }
+    }
+
+    @Override
+    public void start() {
+        logger.info("pulsar sink starting...");
+        sinkCounter.start();
+        pulsarClientService.initCreateConnection(this);
+        if (statIntervalSec > 0) {
+            /*
+             * switch for lots of metrics
+             */
+            if (isNewMetricOn) {
+                monitorIndex = new MonitorIndex("Sink", statIntervalSec, maxMonitorCnt);
+            }
+            monitorIndexExt = new MonitorIndexExt("TDBus_monitors#" + this.getName(),
+                    statIntervalSec, maxMonitorCnt);
+        }
+
+        super.start();
+        this.canSend = true;
+        this.canTake = true;
+
+        try {
+            initTopicSet(new HashSet<String>(topicProperties.values()));
+        } catch (Exception e) {
+            logger.info("meta sink start publish topic fail.",e);
+        }
+
+        for (int i = 0; i < sinkThreadPool.length; i++) {
+            sinkThreadPool[i] = new Thread(new SinkTask(), getName()
+                    + "_pulsar_sink_sender-"
+                    + i);
+            sinkThreadPool[i].start();
+        }
+        logger.debug("meta sink started");
+    }
+
+    @Override
+    public void stop() {
+        logger.info("pulsar sink stopping");
+        pulsarClientService.close();
+        this.canTake = false;
+        int waitCount = 0;
+        while (eventQueue.size() != 0 && waitCount++ < 10) {
+            try {
+                Thread.currentThread().sleep(1000);
+            } catch (InterruptedException e) {
+                logger.info("Stop thread has been interrupt!");
+                break;
+            }
+        }
+        this.canSend = false;
+        if (statIntervalSec > 0) {
+            try {
+                monitorIndex.shutDown();
+            } catch (Exception e) {
+                logger.warn("statrunner interrupted");
+            }
+        }
+        if (sinkThreadPool != null) {
+            for (Thread thread : sinkThreadPool) {
+                if (thread != null) {
+                    thread.interrupt();
+                }
+            }
+            sinkThreadPool = null;
+        }
+        super.stop();
+        if (!scheduledExecutorService.isShutdown()) {
+            scheduledExecutorService.shutdown();
+        }
+        sinkCounter.stop();
+        logger.debug("pulsar sink stopped. Metrics:{}", sinkCounter);
+    }
+
+    @Override
+    public Status process() throws EventDeliveryException {
+        if (!this.canTake) {
+            return Status.BACKOFF;
+        }
+
+        Status status = Status.READY;
+        Channel channel = getChannel();
+        Transaction tx = channel.getTransaction();
+        tx.begin();
+        try {
+            Event event = channel.take();
+            if (event != null) {
+                if (diskRateLimiter != null) {
+                    diskRateLimiter.acquire(event.getBody().length);
+                }
+                if (!eventQueue.offer(event, 3 * 1000, TimeUnit.MILLISECONDS)) {
+                    logger.info("[{}] Channel --> Queue(has no enough space,current code point) "
+                            + "--> pulsar,Check if pulsar server or network is ok.(if this situation "
+                            + "last long time it will cause memoryChannel full and fileChannel write.)", getName());
+                    tx.rollback();
+                } else {
+                    tx.commit();
+                }
+            } else {
+                status = Status.BACKOFF;
+                tx.commit();
+            }
+        } catch (Throwable t) {
+            logger.error("Process event failed!" + this.getName(), t);
+            try {
+                tx.rollback();
+            } catch (Throwable e) {
+                logger.error("metasink transaction rollback exception",e);
+
+            }
+        } finally {
+            tx.close();
+        }
+        return status;
+    }
+
+    private void editStatistic(final Event event, String keyPostfix, String msgId) {
+        String topic = "";
+        String tid = "";
+        String nodeIp = null;
+        if (event != null) {
+            if (event.getHeaders().containsKey(TOPIC)) {
+                topic = event.getHeaders().get(TOPIC);
+            }
+            if (event.getHeaders().containsKey(AttributeConstants.INTERFACE_ID)) {
+                tid = event.getHeaders().get(AttributeConstants.INTERFACE_ID);
+            } else if (event.getHeaders().containsKey(AttributeConstants.INAME)) {
+                tid = event.getHeaders().get(AttributeConstants.INAME);
+            }
+
+            /*
+             * Compatible agent
+             */
+            if (event.getHeaders().containsKey("ip")) {
+                event.getHeaders().put(ConfigConstants.REMOTE_IP_KEY, event.getHeaders().get("ip"));
+                event.getHeaders().remove("ip");
+            }
+
+            /*
+             * Compatible agent
+             */
+            if (event.getHeaders().containsKey("time")) {
+                event.getHeaders().put(AttributeConstants.DATA_TIME,event.getHeaders().get("time"));
+                event.getHeaders().remove("time");
+            }
+
+            if (event.getHeaders().containsKey(ConfigConstants.REMOTE_IP_KEY)) {
+                nodeIp = event.getHeaders().get(ConfigConstants.REMOTE_IP_KEY);
+                if (event.getHeaders().containsKey(ConfigConstants.REMOTE_IDC_KEY)) {
+
+                    if (nodeIp != null) {
+                        nodeIp = nodeIp.split(":")[0];
+                    }
+
+                    long tMsgCounterL = 1L;
+                    /*
+                     * msg counter
+                     */
+                    if (event.getHeaders().containsKey(ConfigConstants.MSG_COUNTER_KEY)) {
+                        tMsgCounterL = Integer.parseInt(event.getHeaders()
+                                .get(ConfigConstants.MSG_COUNTER_KEY));
+                    }
+
+                    /*
+                     * SINK_INTF#metasink1#topic#tid#clientIp#busIP#pkgTime#successCnt#packcnt
+                     * #packsize#failCnt
+                     */
+                    StringBuilder newbase = new StringBuilder();
+                    newbase.append(this.getName()).append(SEPARATOR).append(topic).append(SEPARATOR)
+                            .append(tid).append(SEPARATOR).append(nodeIp)
+                            .append(SEPARATOR).append(NetworkUtils.getLocalIp())
+                            .append(SEPARATOR).append(msgId).append(SEPARATOR)
+                            .append(event.getHeaders().get(ConfigConstants.PKG_TIME_KEY));
+
+                    long messageSize = event.getBody().length;
+
+                    if (event.getHeaders().get(ConfigConstants.TOTAL_LEN) != null) {
+                        messageSize = Long.parseLong(event.getHeaders().get(ConfigConstants.TOTAL_LEN));
+                    }
+
+                    if (keyPostfix != null && !keyPostfix.equals("")) {
+                        monitorIndex.addAndGet(new String(newbase), 0, 0, 0, (int) tMsgCounterL);
+                        if (logPrinterB.shouldPrint()) {
+                            logger.warn("error cannot send event, {} event size is {}", topic, messageSize);
+                        }
+                    } else {
+                        monitorIndex.addAndGet(new String(newbase), (int) tMsgCounterL, 1, messageSize, 0);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void handleCreateClientSuccess(String url) {
+        logger.info("createConnection success for url = {}", url);
+        sinkCounter.incrementConnectionCreatedCount();
+    }
+
+    @Override
+    public void handleCreateClientException(String url) {
+        logger.info("createConnection has exception for url = {}", url);
+        sinkCounter.incrementConnectionFailedCount();
+    }
+
+    @Override
+    public void handleMessageSendSuccess(Object result,  EventStat eventStat) {
+        /*
+         * Statistics pulsar performance
+         */
+        totalPulsarSuccSendCnt.incrementAndGet();
+        totalPulsarSuccSendSize.addAndGet(eventStat.getEvent().getBody().length);
+        /*
+         *add to sinkCounter
+         */
+        sinkCounter.incrementEventDrainSuccessCount();
+        currentInFlightCount.decrementAndGet();
+        currentSuccessSendCnt.incrementAndGet();
+        long nowCnt = currentSuccessSendCnt.get();
+        long oldCnt = lastSuccessSendCnt.get();
+        if (nowCnt % logEveryNEvents == 0 && nowCnt != lastSuccessSendCnt.get()) {
+            lastSuccessSendCnt.set(nowCnt);
+            t2 = System.currentTimeMillis();
+            logger.info("metasink {}, succ put {} events to pulsar,"
+                    + " in the past {} millsec", new Object[] {
+                    getName(), (nowCnt - oldCnt), (t2 - t1)
+            });
+            t1 = t2;
+        }
+        monitorIndexExt.incrementAndGet("METASINK_SUCCESS");
+        editStatistic(eventStat.getEvent(), null, result.toString());
+    }
+
+    @Override
+    public void handleMessageSendException(EventStat eventStat,  Object e) {
+        monitorIndexExt.incrementAndGet("METASINK_EXP");
+        if (e instanceof TooLongFrameException) {
+            PulsarSink.this.overflow = true;
+        } else if (e instanceof ProducerQueueIsFullError) {
+            PulsarSink.this.overflow = true;
+        } else if (!(e instanceof AlreadyClosedException
+                || e instanceof NotConnectedException
+                || e instanceof TopicTerminatedException)) {
+            if (logPrinterB.shouldPrint()) {
+                logger.error("Send failed!{}{}", getName(), e);
+            }
+            if (eventStat.getRetryCnt() == 0) {
+                editStatistic(eventStat.getEvent(), "failure", "");
+            }
+        }
+        eventStat.incRetryCnt();
+        resendEvent(eventStat, true);
+    }
+
+    /**
+     * Resend the data and store the data in the memory cache.
+     * @param es
+     * @param isDecrement
+     */
+    private void resendEvent(EventStat es, boolean isDecrement) {
+        try {
+            if (isDecrement) {
+                currentInFlightCount.decrementAndGet();
+            }
+            if (es == null || es.getEvent() == null) {
+                return;
+            }
+            /*
+             * If the failure requires retransmission to pulsar,
+             * the sid needs to be removed before retransmission.
+             */
+            if (clientIdCache) {
+                String clientId = es.getEvent().getHeaders().get(ConfigConstants.SEQUENCE_ID);
+                if (clientId != null && agentIdCache.asMap().containsKey(clientId)) {
+                    agentIdCache.invalidate(clientId);
+                }
+            }
+            if (!resendQueue.offer(es)) {
+                /*
+                 * If the retry queue is full, it will re-enter the channel; if it is not full,
+                 *  it will be taken out of the resendQueue in sinktask
+                 */
+                FailoverChannelProcessorHolder.getChannelProcessor().processEvent(es.getEvent());
+                if (logPrinterC.shouldPrint()) {
+                    logger.error(getName() + " Channel --> pulsar --> ResendQueue(full) "
+                            + "-->FailOverChannelProcessor(current code point), "
+                            + "Resend queue is full,Check if pulsar server or network is ok.");
+                }
+            }
+        } catch (Throwable throwable) {
+            monitorIndexExt.incrementAndGet("PULSAR_SINK_DROPPED");
+            if (logPrinterC.shouldPrint()) {
+                logger.error(getName() + " Discard msg because put events to both of "
+                        + "queue and fileChannel fail,current resendQueue.size = "
+                        + resendQueue.size(), throwable);
+            }
+        }
+    }
+
+    static class PulsarPerformanceTask implements Runnable {
+        @Override
+        public void run() {
+            try {
+                if (totalPulsarSuccSendSize.get() != 0) {
+                    logger.info("Total pulsar performance tps :"
+                            + totalPulsarSuccSendCnt.get() / PRINT_INTERVAL
+                            + "/s, avg msg size:"
+                            + totalPulsarSuccSendSize.get() / totalPulsarSuccSendCnt.get()
+                            + ",print every " + PRINT_INTERVAL + " seconds");
+                    /*
+                     * totalpulsarSuccSendCnt represents the number of packets
+                     */
+                    totalPulsarSuccSendCnt.set(0);
+                    totalPulsarSuccSendSize.set(0);
+                }
+
+            } catch (Exception e) {
+                logger.info("pulsarPerformanceTask error", e);
+            }
+        }
+    }
+
+    class SinkTask implements Runnable {
+        @Override
+        public void run() {
+            logger.info("Sink task {} started.", Thread.currentThread().getName());
+            while (canSend) {
+                boolean decrementFlag = false;
+                Event event = null;
+                EventStat eventStat = null;
+                String topic = null;
+                try {
+                    if (PulsarSink.this.overflow) {
+                        PulsarSink.this.overflow = false;
+                        Thread.currentThread().sleep(10);
+                    }
+                    if (!resendQueue.isEmpty()) {
+                        /*
+                         * Send the data in the retry queue first
+                         */
+                        eventStat = resendQueue.poll();
+                        if (eventStat != null) {
+                            event = eventStat.getEvent();
+                            // logger.warn("Resend event: {}", event.toString());
+                            if (event.getHeaders().containsKey(TOPIC)) {
+                                topic = event.getHeaders().get(TOPIC);
+                            }
+                        }
+                    } else {
+                        if (currentInFlightCount.get() > BATCH_SIZE) {
+                            /*
+                             * Under the condition that the number of unresponsive messages
+                             * is greater than 1w, the number of unresponsive messages sent
+                             * to pulsar will be printed periodically
+                             */
+                            logCounter++;
+                            if (logCounter == 1 || logCounter % 100000 == 0) {
+                                logger.info(getName()
+                                                + " currentInFlightCount={} resendQueue"
+                                                + ".size={}",
+                                        currentInFlightCount.get(),resendQueue.size());
+                            }
+                            if (logCounter > Long.MAX_VALUE - 10) {
+                                logCounter = 0;
+                            }
+                        }
+                        event = eventQueue.take();
+                        eventStat = new EventStat(event);
+                        sinkCounter.incrementEventDrainAttemptCount();
+                        if (event.getHeaders().containsKey(TOPIC)) {
+                            topic = event.getHeaders().get(TOPIC);
+                        }
+                    }
+                    logger.debug("Event is {}, topic = {} ",event, topic);
+
+                    if (event == null) {
+                        continue;
+                    }
+
+                    if (topic == null || topic.equals("")) {
+                        logger.warn("no topic specified in event header, just skip this event");
+                        continue;
+                    }
+
+                    Long expireTime = illegalTopicMap.get(topic);
+                    if (expireTime != null) {
+                        long currentTime = System.currentTimeMillis();
+                        if (expireTime > currentTime) {
+                            /*
+                             * If the exception-channel is configured, put the exception-channel,
+                             * otherwise discard the data
+                             */
+                            continue;
+                        } else {
+                            /*
+                             * Illegal has expired, there is no need to put it in the map
+                             */
+                            illegalTopicMap.remove(topic);
+                        }
+                    }
+
+                    String clientId = event.getHeaders().get(ConfigConstants.SEQUENCE_ID);
+                    final EventStat es = eventStat;
+
+                    boolean hasKey = false;
+                    if (clientIdCache && clientId != null) {
+                        hasKey = agentIdCache.asMap().containsKey(clientId);
+                    }
+
+                    if (clientIdCache && clientId != null && hasKey) {
+                        agentIdCache.put(clientId, System.currentTimeMillis());
+                        if (logPrinterA.shouldPrint()) {
+                            logger.info("{} agent package {} existed,just discard.",
+                                    getName(), clientId);
+                        }
+                    } else {
+                        if (clientId != null) {
+                            agentIdCache.put(clientId, System.currentTimeMillis());
+                        }
+                        boolean sendResult = pulsarClientService.sendMessage(topic, event,
+                                PulsarSink.this, es);
+                        /*
+                         * handle producer is current is null
+                         */
+                        if (!sendResult) {
+                            illegalTopicMap.put(topic, System.currentTimeMillis() + 30 * 1000);
+                            continue;
+                        }
+                        currentInFlightCount.incrementAndGet();
+                        decrementFlag = true;
+                    }
+                    /*
+                     * No exception is thrown, after a complete one-time sending,
+                     * the topic can be deleted from the illegal list
+                     */
+                    illegalTopicMap.remove(topic);
+                } catch (InterruptedException e) {
+                    logger.info("Thread {} has been interrupted!", Thread.currentThread().getName());
+                    return;
+                } catch (Throwable t) {
+                    if (t instanceof PulsarClientException) {
+                        String message = t.getMessage();
+                        if (message != null && (message.contains("No available queue for topic")
+                                || message.contains("The brokers of topic are all forbidden"))) {
+                            illegalTopicMap.put(topic, System.currentTimeMillis() + 60 * 1000);
+                            logger.info("IllegalTopicMap.put " + topic);
+                            continue;
+                        } else {
+                            try {
+                                /*
+                                 * The exception of pulsar will cause the sending thread to block
+                                 * and prevent further pressure on pulsar. Here you should pay
+                                 * attention to the type of exception to prevent the error of
+                                 *  a topic from affecting the global
+                                 */
+                                Thread.sleep(100);
+                            } catch (InterruptedException e) {
+                                //ignore..
+                            }
+                        }
+                    }
+                    if (logPrinterA.shouldPrint()) {
+                        logger.error("Sink task fail to send the message, decrementFlag="
+                                + decrementFlag
+                                + ",sink.name="
+                                + Thread.currentThread().getName()
+                                + ",event.headers="
+                                + eventStat.getEvent().getHeaders(), t);
+                    }
+                    /*
+                     * producer.sendMessage is abnormal,
+                     * so currentInFlightCount is not added,
+                     * so there is no need to subtract
+                     */
+                    resendEvent(eventStat, decrementFlag);
+                }
+            }
+        }
+    }
+}
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/CreatePulsarClientCallBack.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/CreatePulsarClientCallBack.java
new file mode 100644
index 0000000..d2c6c67
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/CreatePulsarClientCallBack.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.sink.pulsar;
+
+public interface CreatePulsarClientCallBack {
+
+    void handleCreateClientSuccess(String url);
+
+    void handleCreateClientException(String url);
+}
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/PulsarClientService.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/PulsarClientService.java
new file mode 100644
index 0000000..7aae1b6
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/PulsarClientService.java
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.sink.pulsar;
+
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.flume.Context;
+import org.apache.flume.Event;
+import org.apache.flume.FlumeException;
+import org.apache.inlong.dataproxy.consts.AttributeConstants;
+import org.apache.inlong.dataproxy.consts.ConfigConstants;
+import org.apache.inlong.dataproxy.sink.EventStat;
+import org.apache.inlong.dataproxy.utils.LogCounter;
+import org.apache.inlong.dataproxy.utils.NetworkUtils;
+import org.apache.pulsar.client.api.Producer;
+import org.apache.pulsar.client.api.PulsarClient;
+import org.apache.pulsar.client.api.PulsarClientException;
+import org.apache.pulsar.client.impl.MessageIdImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PulsarClientService {
+
+    private static final Logger logger = LoggerFactory.getLogger(PulsarClientService.class);
+
+    private static final LogCounter logPrinterA = new LogCounter(10, 100000, 60 * 1000);
+
+    /*
+     * properties key for pulsar client
+     */
+    private static String PULSAR_SERVER_URL_LIST = "pulsar_server_url_list";
+
+    /*
+     * properties key pulsar producer
+     */
+    private static String SEND_TIMEOUT = "send_timeout_mill";
+    private static String CLIENT_TIMEOUT = "client_timeout_second";
+    private static String ENABLE_BATCH = "enable_batch";
+    private static String BLOCK_IF_QUEUE_FULL = "block_if_queue_full";
+    private static String MAX_PENDING_MESSAGES = "max_pending_messages";
+    private static String MAX_BATCHING_MESSAGES = "max_batching_messages";
+    private static String RETRY_INTERVAL_WHEN_SEND_ERROR_MILL = "retry_interval_when_send_error_ms";
+
+    private static int DEFAULT_SEND_TIMEOUT_MILL = 30 * 1000;
+    private static int DEFAULT_CLIENT_TIMEOUT_SECOND = 30;
+    private static long DEFAULT_RETRY_INTERVAL_WHEN_SEND_ERROR_MILL = 30 * 1000L;
+    private static boolean DEFAULT_ENABLE_BATCH = true;
+    private static boolean DEFAULT_BLOCK_IF_QUEUE_FULL = true;
+    private static int DEFAULT_MAX_PENDING_MESSAGES = 10000;
+    private static int DEFAULT_MAX_BATCHING_MESSAGES = 1000;
+
+    /*
+     * for pulsar client
+     */
+    private String[] pulsarServerUrls;
+
+    /*
+     * for producer
+     */
+    private Integer sendTimeout; // in millsec
+    private Integer clientTimeout;
+    private boolean enableBatch = true;
+    private boolean blockIfQueueFull = true;
+    private int maxPendingMessages = 10000;
+    private int maxBatchingMessages = 1000;
+    private long retryIntervalWhenSendMsgError = 30 * 1000L;
+    public Map<String, List<TopicProducerInfo>> producerInfoMap;
+    public Map<String, AtomicLong> topicSendIndexMap;
+    public List<PulsarClient> pulsarClients;
+
+    private String localIp = "127.0.0.1";
+
+    public PulsarClientService(Context context) {
+
+        String pulsarServerUrlList = context.getString(PULSAR_SERVER_URL_LIST);
+        Preconditions.checkState(pulsarServerUrlList != null, "No pulsar server url specified");
+        pulsarServerUrls = pulsarServerUrlList.split("\\|");
+        Preconditions.checkState(pulsarServerUrls != null && pulsarServerUrls.length > 0, "No "
+                + "pulsar server url config");
+        sendTimeout = context.getInteger(SEND_TIMEOUT, DEFAULT_SEND_TIMEOUT_MILL);
+        retryIntervalWhenSendMsgError = context.getLong(RETRY_INTERVAL_WHEN_SEND_ERROR_MILL,
+                DEFAULT_RETRY_INTERVAL_WHEN_SEND_ERROR_MILL);
+        clientTimeout = context.getInteger(CLIENT_TIMEOUT, DEFAULT_CLIENT_TIMEOUT_SECOND);
+        logger.debug("PulsarClientService " + SEND_TIMEOUT + " " + sendTimeout);
+        Preconditions.checkArgument(sendTimeout > 0, "sendTimeout must be > 0");
+
+        enableBatch = context.getBoolean(ENABLE_BATCH, DEFAULT_ENABLE_BATCH);
+        blockIfQueueFull = context.getBoolean(BLOCK_IF_QUEUE_FULL, DEFAULT_BLOCK_IF_QUEUE_FULL);
+        maxPendingMessages = context.getInteger(MAX_PENDING_MESSAGES, DEFAULT_MAX_PENDING_MESSAGES);
+        maxBatchingMessages =  context.getInteger(MAX_BATCHING_MESSAGES, DEFAULT_MAX_BATCHING_MESSAGES);
+        producerInfoMap = new ConcurrentHashMap<String, List<TopicProducerInfo>>();
+        topicSendIndexMap = new ConcurrentHashMap<String, AtomicLong>();
+        localIp = NetworkUtils.getLocalIp();
+
+    }
+
+    public void initCreateConnection(CreatePulsarClientCallBack callBack) {
+        try {
+            createConnection(callBack);
+        } catch (FlumeException e) {
+            logger.error("Unable to create pulsar client" + ". Exception follows.", e);
+            close();
+        }
+    }
+
+    public boolean sendMessage(String topic, Event event,
+            SendMessageCallBack sendMessageCallBack, EventStat es) {
+        TopicProducerInfo producer = null;
+        try {
+            producer = getProducer(topic);
+        } catch (Exception e) {
+            if (logPrinterA.shouldPrint()) {
+                /*
+                 * If it is not an IllegalTopicException,
+                 * the producer may be null,
+                 * causing the sendMessage part to report a null pointer later
+                 */
+                logger.error("Get producer failed!", e);
+            }
+        }
+        /*
+         * If the producer is a null value,\ it means that the topic is not yet
+         * ready, and it needs to be played back into the file channel
+         */
+        if (producer == null) {
+            /*
+             * Data within 30s is placed in the exception channel to
+             * prevent frequent checks
+             * After 30s, reopen the topic check, if it is still a null value,
+             *  put it back into the illegal map
+             */
+            return false;
+        }
+
+        Map<String, String> proMap = new HashMap<>();
+        proMap.put("tdbusip", localIp);
+        String tid = "";
+        if (event.getHeaders().containsKey(AttributeConstants.INTERFACE_ID)) {
+            tid = event.getHeaders().get(AttributeConstants.INTERFACE_ID);
+        } else if (event.getHeaders().containsKey(AttributeConstants.INAME)) {
+            tid = event.getHeaders().get(AttributeConstants.INAME);
+        }
+        proMap.put(tid, event.getHeaders().get(ConfigConstants.PKG_TIME_KEY));
+        logger.debug("producer send msg!");
+        TopicProducerInfo forCallBackP = producer;
+        forCallBackP.getProducer().newMessage().properties(proMap).value(event.getBody())
+                .sendAsync().thenAccept((msgId) -> {
+            forCallBackP.setCanUseSend(true);
+            sendMessageCallBack.handleMessageSendSuccess((MessageIdImpl)msgId, es);
+
+        }).exceptionally((e) -> {
+            forCallBackP.setCanUseSend(false);
+            sendMessageCallBack.handleMessageSendException(es, e);
+            return null;
+        });
+        return true;
+    }
+
+    /**
+     * If this function is called successively without calling {@see #destroyConnection()}, only the
+     * first call has any effect.
+     *
+     * @throws FlumeException if an RPC client connection could not be opened
+     */
+    private void createConnection(CreatePulsarClientCallBack callBack) throws FlumeException {
+        if (pulsarClients != null) {
+            return;
+        }
+        pulsarClients = new ArrayList<PulsarClient>();
+        for (int i = 0; i < pulsarServerUrls.length; i++) {
+            try {
+                logger.debug("index = {}, url = {}", i, pulsarServerUrls[i]);
+                PulsarClient client = initPulsarClient(pulsarServerUrls[i]);
+                pulsarClients.add(client);
+                callBack.handleCreateClientSuccess(pulsarServerUrls[i]);
+            } catch (PulsarClientException e) {
+                callBack.handleCreateClientException(pulsarServerUrls[i]);
+                logger.error("create connnection error in metasink, "
+                        + "maybe pulsar master set error, please re-check.url{}, ex1 {}",
+                        pulsarServerUrls[i],
+                        e.getMessage());
+            } catch (Throwable e) {
+                callBack.handleCreateClientException(pulsarServerUrls[i]);
+                logger.error("create connnection error in metasink, "
+                                + "maybe pulsar master set error/shutdown in progress, please "
+                                + "re-check. url{}, ex2 {}",
+                        pulsarServerUrls[i],
+                        e.getMessage());
+            }
+        }
+        if (pulsarClients.size() == 0) {
+            throw new FlumeException("connect to pulsar error1, "
+                    + "maybe zkstr/zkroot set error, please re-check");
+        }
+    }
+
+    private PulsarClient initPulsarClient(String pulsarUrl) throws Exception {
+        return PulsarClient.builder()
+                .serviceUrl(pulsarUrl)
+                .connectionTimeout(clientTimeout, TimeUnit.SECONDS)
+                .build();
+    }
+
+    public List<TopicProducerInfo> initTopicProducer(String topic) {
+        logger.info("initTopicProducer topic = {}", topic);
+        List<TopicProducerInfo> producerInfoList = producerInfoMap.computeIfAbsent(topic, (k) -> {
+            List<TopicProducerInfo> newList = new ArrayList<>();
+            if (pulsarClients != null) {
+                for (PulsarClient pulsarClient : pulsarClients) {
+                    TopicProducerInfo info = new TopicProducerInfo(pulsarClient, topic);
+                    info.initProducer();
+                    newList.add(info);
+                }
+            }
+            return newList;
+        });
+        return producerInfoList;
+    }
+
+    private TopicProducerInfo getProducer(String topic) {
+        List<TopicProducerInfo> producerList = initTopicProducer(topic);
+        AtomicLong topicIndex = topicSendIndexMap.computeIfAbsent(topic,(k) -> {
+            return new AtomicLong(0);
+        });
+        int maxTryToGetProducer = producerList.size();
+        if (maxTryToGetProducer == 0) {
+            return null;
+        }
+        int retryTime = 0;
+        TopicProducerInfo p = null;
+        do {
+            int index = (int)(topicIndex.getAndIncrement() % maxTryToGetProducer);
+            p = producerList.get(index);
+            if (p.isCanUseToSendMessage() && p.getProducer().isConnected()) {
+                break;
+            }
+            retryTime++;
+        } while (retryTime < maxTryToGetProducer);
+        return p;
+    }
+
+    public Map<String, List<TopicProducerInfo>> getProducerInfoMap() {
+        return producerInfoMap;
+    }
+
+    private void destroyConnection() {
+        producerInfoMap.clear();
+        if (pulsarClients != null) {
+            for (PulsarClient pulsarClient : pulsarClients) {
+                try {
+                    pulsarClient.shutdown();
+                } catch (PulsarClientException e) {
+                    logger.error("destroy pulsarClient error in PulsarSink, PulsarClientException {}",
+                            e.getMessage());
+                } catch (Exception e) {
+                    logger.error("destroy pulsarClient error in PulsarSink, ex {}", e.getMessage());
+                }
+            }
+        }
+        pulsarClients = null;
+        logger.debug("closed meta producer");
+    }
+
+    public void close() {
+        destroyConnection();
+    }
+
+    class TopicProducerInfo {
+        private long lastSendMsgErrorTime;
+
+        private Producer producer;
+
+        private PulsarClient pulsarClient;
+
+        private String topic;
+
+        private volatile Boolean isCanUseSend = true;
+
+        private volatile Boolean isFinishInit = false;
+
+        public TopicProducerInfo(PulsarClient pulsarClient,
+                String topic) {
+            this.pulsarClient = pulsarClient;
+            this.topic = topic;
+        }
+
+        public void initProducer() {
+            try {
+                producer = pulsarClient.newProducer().sendTimeout(sendTimeout,
+                        TimeUnit.MILLISECONDS)
+                        .topic(topic)
+                        .enableBatching(enableBatch)
+                        .blockIfQueueFull(blockIfQueueFull)
+                        .maxPendingMessages(maxPendingMessages)
+                        .batchingMaxMessages(maxBatchingMessages)
+                        .create();
+                isFinishInit = true;
+            } catch (PulsarClientException e) {
+                logger.error("create pulsar client has error e = {}", e);
+                isFinishInit = false;
+            }
+        }
+
+        public void setCanUseSend(Boolean isCanUseSend) {
+            this.isCanUseSend = isCanUseSend;
+            if (!isCanUseSend) {
+                lastSendMsgErrorTime = System.currentTimeMillis();
+            }
+        }
+
+        public boolean isCanUseToSendMessage() {
+            if (isCanUseSend && isFinishInit) {
+                return true;
+            } else if (isFinishInit && (System.currentTimeMillis() - lastSendMsgErrorTime)
+                    > retryIntervalWhenSendMsgError) {
+                lastSendMsgErrorTime = System.currentTimeMillis();
+                return true;
+            }
+            return false;
+        }
+
+        public Producer getProducer() {
+            return producer;
+        }
+    }
+}
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/SendMessageCallBack.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/SendMessageCallBack.java
new file mode 100644
index 0000000..66323e7
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/sink/pulsar/SendMessageCallBack.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.sink.pulsar;
+
+import org.apache.inlong.dataproxy.sink.EventStat;
+
+public interface SendMessageCallBack {
+
+    void handleMessageSendSuccess(Object msgId, EventStat es);
+
+    void handleMessageSendException(EventStat es, Object exception);
+}
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/source/SimpleTcpSource.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/source/SimpleTcpSource.java
index 94a3b26..59df0a6 100644
--- a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/source/SimpleTcpSource.java
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/source/SimpleTcpSource.java
@@ -356,7 +356,7 @@ public class SimpleTcpSource extends AbstractSource implements Configurable, Eve
 
         messageHandlerName =
                 context.getString(ConfigConstants.MESSAGE_HANDLER_NAME,
-                        "org.apache.flume.source.ServerMessageHandler");
+                        "org.apache.inlong.dataproxy.source.ServerMessageHandler");
         messageHandlerName = messageHandlerName.trim();
         Preconditions.checkArgument(StringUtils.isNotBlank(messageHandlerName),
                 "messageHandlerName is empty");
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/FailoverChannelProcessorHolder.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/FailoverChannelProcessorHolder.java
new file mode 100644
index 0000000..4757bde
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/FailoverChannelProcessorHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.utils;
+
+import org.apache.flume.channel.ChannelProcessor;
+
+public class FailoverChannelProcessorHolder {
+    private static ChannelProcessor channelProcessor;
+
+    public static ChannelProcessor getChannelProcessor() {
+        return channelProcessor;
+    }
+
+    public static void setChannelProcessor(ChannelProcessor cp) {
+        channelProcessor = cp;
+    }
+}
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/LogCounter.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/LogCounter.java
new file mode 100644
index 0000000..9e45827
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/LogCounter.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.utils;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class LogCounter {
+
+    private AtomicInteger counter = new AtomicInteger(0);
+
+    private int start = 10;
+    private int control = 1000;
+    private int reset = 60 * 1000;
+
+    private long lastLogTime = System.currentTimeMillis();
+
+    public LogCounter(int start, int control, int reset) {
+        this.start = start;
+        this.control = control;
+        this.reset = reset;
+    }
+
+    public boolean shouldPrint() {
+        if (System.currentTimeMillis() - lastLogTime > reset) {
+            counter.set(0);
+            this.lastLogTime = System.currentTimeMillis();
+        }
+
+        if (counter.incrementAndGet() > start && counter.get() % control != 0) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/MonitorIndex.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/MonitorIndex.java
new file mode 100644
index 0000000..b140677
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/MonitorIndex.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MonitorIndex {
+    private static final Logger logger = LoggerFactory.getLogger(MonitorIndex.class);
+    private static final LogCounter logPrinter = new LogCounter(10, 100000, 60 * 1000);
+
+    private IndexCollectThread indexCol;
+    private String name;
+    private ConcurrentHashMap<String, String> counterMap = new ConcurrentHashMap<String, String>();
+    private int intervalSec;
+    private int maxCnt;
+
+    public MonitorIndex(String name, int intervalSec, int maxCnt) {
+        /*
+         * Main indicators, placed in flume_index.log
+         */
+        this.intervalSec = intervalSec;
+        /*
+         * "TDBus","TDBus_intf"...tmetric pattern name
+         */
+        this.name = name;
+        this.maxCnt = maxCnt;
+        indexCol = new IndexCollectThread();
+        indexCol.setDaemon(true);
+        indexCol.setName("IndexCollectThread_MonitorIndex");
+        indexCol.start();
+    }
+
+    public void addAndGet(String key, int cnt, int packcnt, long packsize, int failcnt) {
+        try {
+            if (counterMap.size() < maxCnt) {
+                counterMap.compute(key, (key1, value) -> {
+                    if (value != null) {
+                        String[] va = value.split("#");
+                        value = (Integer.parseInt(va[0]) + cnt) + "#"
+                                + (Integer.parseInt(va[1]) + packcnt) + "#"
+                                + (Long.parseLong(va[2]) + packsize) + "#"
+                                + (Integer.parseInt(va[3]) + failcnt);
+                    } else {
+                        StringBuilder stringBuilder = new StringBuilder();
+                        value = stringBuilder.append(cnt).append("#").append(packcnt).append("#")
+                            .append(packsize).append("#").append(failcnt).toString();
+                    }
+                    return value;
+                });
+            } else if (logPrinter.shouldPrint()) {
+                logger.error(this.name + "exceed monitor's max size");
+            }
+        } catch (Exception e) {
+            if (logPrinter.shouldPrint()) {
+                logger.error("monitor exception",e);
+            }
+        }
+    }
+
+    public void shutDown() {
+        indexCol.shutDown();
+    }
+
+    public int getMaxCnt() {
+        return maxCnt;
+    }
+
+    public void setMaxCnt(int maxCnt) {
+        this.maxCnt = maxCnt;
+    }
+
+    private class IndexCollectThread extends Thread {
+        private boolean bShutDown = false;
+
+        public IndexCollectThread() {
+            bShutDown = false;
+        }
+
+        public void shutDown() {
+            bShutDown = true;
+        }
+
+        @Override
+        public void run() {
+            Map<String, String> counterExt = new HashMap<String, String>();
+            while (!bShutDown) {
+                try {
+                    Thread.sleep(intervalSec * 1000L);
+                    for (String str : counterMap.keySet()) {
+                        counterMap.computeIfPresent(str, (s, s2) -> {
+                            counterExt.put(s, s2);
+                            return null;
+                        });
+                    }
+                    for (Map.Entry<String, String> entrys : counterExt.entrySet()) {
+                        logger.info(
+                                "{}#{}#{}", new Object[] { name, entrys.getKey(), entrys.getValue() });
+                    }
+                    counterExt.clear();
+                } catch (Exception e) {
+                    logger.warn("monitor interrupted");
+                }
+            }
+
+        }
+    }
+}
+
diff --git a/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/MonitorIndexExt.java b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/MonitorIndexExt.java
new file mode 100644
index 0000000..959680e
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/main/java/org/apache/inlong/dataproxy/utils/MonitorIndexExt.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MonitorIndexExt {
+    private static final Logger logger = LoggerFactory.getLogger(MonitorIndexExt.class);
+    private static final LogCounter logPrinter = new LogCounter(10, 100000, 60 * 1000);
+
+    private IndexCollectThread indexCol;
+    private String name;
+    private ConcurrentHashMap<String, AtomicLong> counterMap = new ConcurrentHashMap<String, AtomicLong>();
+    private int intervalSec;
+    private int maxCnt;
+
+    public MonitorIndexExt(String name, int intervalSec, int maxCnt) {
+        /*
+         * key
+         * Almost unchanging. Component indicators, flume_monitors.log
+         */
+        this.intervalSec = intervalSec;
+        /*
+         * TDBus_monitors", used to monitor...tmetric pattern name
+         */
+        this.name = name;
+        this.maxCnt = maxCnt;
+        indexCol = new IndexCollectThread();
+        indexCol.setDaemon(true);
+        indexCol.setName("IndexCollectThread_MonitorIndexExt");
+        indexCol.start();
+    }
+
+    public void incrementAndGet(String key) {
+        try {
+            if (counterMap.size() < maxCnt) {
+                counterMap.compute(key, (s, atomicLong) -> {
+                    if (atomicLong != null) {
+                        atomicLong.incrementAndGet();
+                    } else {
+                        atomicLong = new AtomicLong(1);
+                    }
+                    return atomicLong;
+                });
+            } else if (logPrinter.shouldPrint()) {
+                logger.error(this.name + "exceed monitorExt's max size");
+            }
+        } catch (Exception e) {
+            if (logPrinter.shouldPrint()) {
+                logger.error("monitorExt exception",e);
+            }
+        }
+    }
+
+    public void shutDown() {
+        indexCol.shutDown();
+    }
+
+    public int getMaxCnt() {
+        return maxCnt;
+    }
+
+    public void setMaxCnt(int maxCnt) {
+        this.maxCnt = maxCnt;
+    }
+
+    private class IndexCollectThread extends Thread {
+        private boolean bShutDown = false;
+
+        public IndexCollectThread() {
+            bShutDown = false;
+        }
+
+        public void shutDown() {
+            bShutDown = true;
+        }
+
+        @Override
+        public void run() {
+            Map<String, Long> counterExt = new HashMap<String, Long>();
+            while (!bShutDown) {
+                try {
+                    Thread.sleep(intervalSec * 1000L);
+                    for (String str : counterMap.keySet()) {
+                        counterMap.computeIfPresent(str, (s, atomicLong) -> {
+                            long cnt = atomicLong.get();
+                            counterExt.put(str, cnt);
+                            atomicLong.set(0L);
+                            return atomicLong;
+                        });
+                    }
+
+                    for (Map.Entry<String, Long> entrys : counterExt.entrySet()) {
+                        logger.info("{}#{}#{}", new Object[] { name, entrys.getKey(), entrys.getValue() });
+                    }
+                    counterExt.clear();
+
+                } catch (Exception e) {
+                    logger.warn("moniorExt interrupted");
+                }
+            }
+        }
+    }
+}
+
diff --git a/inlong-dataproxy/dataproxy-source/src/main/resources/flume.conf b/inlong-dataproxy/dataproxy-source/src/main/resources/flume.conf
index d0aa15e..c369630 100644
--- a/inlong-dataproxy/dataproxy-source/src/main/resources/flume.conf
+++ b/inlong-dataproxy/dataproxy-source/src/main/resources/flume.conf
@@ -266,6 +266,16 @@ agent1.sinks.meta-sink-msg8.old-metric-on=true
 agent1.sinks.meta-sink-msg8.set=10
 agent1.sinks.meta-sink-msg8.disk-io-rate-per-sec=20000000
 
+
+agent1.sinks.pulsar-sink-msg1.channel = ch-msg1
+agent1.sinks.pulsar-sink-msg1.type = org.apache.inlong.dataproxy.sink.PulsarSink
+agent1.sinks.pulsar-sink-msg1.pulsar_server_url_list = pulsar://127.0.0.1:6650|pulsar://127.0.0.2:6650
+agent1.sinks.pulsar-sink-msg1.send_timeout_MILL = 30000
+agent1.sinks.pulsar-sink-msg1.stat_interval_sec = 60
+agent1.sinks.pulsar-sink-msg1.thread-num = 8
+agent1.sinks.pulsar-sink-msg1.client-id-cache = true
+agent1.sinks.pulsar-sink-msg1.disk-io-rate-per-sec=20000000
+
 agent1.sources = tcp-source
 agent1.channels = ch-back ch-msg1 ch-msg2 ch-msg3 ch-msg5 ch-msg6 ch-msg7 ch-msg8 ch-msg9 ch-msg10
 agent1.sinks = meta-sink-msg1 meta-sink-msg2 meta-sink-msg3 meta-sink-msg5 meta-sink-msg6 meta-sink-msg7 meta-sink-msg8 meta-sink-transfer
diff --git a/inlong-dataproxy/dataproxy-source/src/test/java/org/apache/inlong/dataproxy/sink/TestPulsarSink.java b/inlong-dataproxy/dataproxy-source/src/test/java/org/apache/inlong/dataproxy/sink/TestPulsarSink.java
new file mode 100644
index 0000000..a292840
--- /dev/null
+++ b/inlong-dataproxy/dataproxy-source/src/test/java/org/apache/inlong/dataproxy/sink/TestPulsarSink.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.dataproxy.sink;
+
+import com.google.common.base.Charsets;
+import org.apache.flume.Channel;
+import org.apache.flume.Context;
+import org.apache.flume.Event;
+import org.apache.flume.EventDeliveryException;
+import org.apache.flume.Sink;
+import org.apache.flume.Transaction;
+import org.apache.flume.channel.MemoryChannel;
+import org.apache.flume.conf.Configurables;
+import org.apache.flume.event.EventBuilder;
+import org.apache.flume.lifecycle.LifecycleController;
+import org.apache.flume.lifecycle.LifecycleState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.testng.Assert;
+
+public class TestPulsarSink {
+    private static final Logger logger = LoggerFactory
+            .getLogger(TestPulsarSink.class);
+    private static final String hostname = "127.0.0.1";
+    private static final Integer port = 1234;
+    private String zkStr = "127.0.0.1:2181";
+    private String zkRoot = "/meta";
+    private int batchSize = 1;
+
+    private PulsarSink sink;
+    private Channel channel;
+
+    @BeforeClass
+    public void setUp() {
+        sink = new PulsarSink();
+        channel = new MemoryChannel();
+
+        Context context = new Context();
+
+        context.put("type", "org.apache.inlong.dataproxy.sink.PulsarSink");
+        context.put("pulsar_server_url_list", "pulsar://127.0.0.1:6650");
+
+        sink.setChannel(channel);
+
+        Configurables.configure(sink, context);
+        Configurables.configure(channel, context);
+    }
+
+    @Test
+    public void testProcess() throws InterruptedException, EventDeliveryException,
+            InstantiationException, IllegalAccessException {
+        setUp();
+        Event event = EventBuilder.withBody("test event 1", Charsets.UTF_8);
+        sink.start();
+        Assert.assertTrue(LifecycleController.waitForOneOf(sink,
+                LifecycleState.START_OR_ERROR, 5000));
+
+        Transaction transaction = channel.getTransaction();
+
+        transaction.begin();
+        for (int i = 0; i < 10; i++) {
+            channel.put(event);
+        }
+        transaction.commit();
+        transaction.close();
+
+        for (int i = 0; i < 5; i++) {
+            Sink.Status status = sink.process();
+            Assert.assertEquals(Sink.Status.READY, status);
+        }
+
+        sink.stop();
+        Assert.assertTrue(LifecycleController.waitForOneOf(sink,
+                LifecycleState.STOP_OR_ERROR, 5000));
+    }
+
+}
diff --git a/inlong-dataproxy/pom.xml b/inlong-dataproxy/pom.xml
index d73a77b..cbf59ab 100644
--- a/inlong-dataproxy/pom.xml
+++ b/inlong-dataproxy/pom.xml
@@ -46,6 +46,8 @@
         <codec.version>1.15</codec.version>
         <servlet.version>2.5-20110124</servlet.version>
         <inlong-common.version>1.3.4</inlong-common.version>
+        <pulsar.version>2.8.1</pulsar.version>
+        <testng.version>6.14.3</testng.version>
     </properties>
 
     <dependencies>
@@ -94,6 +96,17 @@
             <artifactId>inlong-common</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.pulsar</groupId>
+            <artifactId>pulsar-client</artifactId>
+            <version>${pulsar.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>${testng.version}</version>
+        </dependency>
     </dependencies>
 
 </project>
\ No newline at end of file