RabbitMQ(四)-- 限流和监控方式

消费端限流

假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!

当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。

TTL

Time To Live,消息过期时间设置
声明队列时,指定即可

TTL:过期时间

  1. 队列统一过期
  2. 消息单独过期

如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。

  • 队列过期后,会将队列所有消息全部移除

  • 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)

死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,被重新发送到另一个交换机,这个交换机就是DLX

消息成为死信的三种情况:

  1. 队列消息长度到达限制;

  2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;

  3. 原队列存在消息过期设置,消息到达超时时间未被消费;

队列绑定死信交换机:

给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key

也就是说此时Queue作为"生产者"

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费

需求:

下单后,30分钟未支付,取消订单,回滚库存。

新用户注册成功7天后,发送短信问候。

实现方式:

定时器 (×)

延迟队列 (√)

实现步骤:

在RabbitMQ中并未提供延迟队列功能

替代实现: TTL+死信队列 组合实现延迟队列的效果

设置队列过期时间30分钟,当30分钟过后,消息未被消费,进入死信队列,路由到指定队列,调用库存系统,判断订单状态。

RabbitMQ 监控

了解 RabbitMQ 的运行状态,主要有三种途径:Management UI,rabbitmqctl 命令和 REST API 以及使用 prometheus + grafana,当然大厂的话都会自定开发基于 api监控系统

  • Management UI

RabbitMQ 给我们提供了丰富的 Web 管理功能,通过页面,我们能看到 RabbitMQ 的整体运行状况,交换机和队列的状态等,还可以进行人员管理和权限配置,相当全面。

但如果想通过页面来监控,那出不出问题只能靠缘分。看到出问题了,是运气好,看不到出问题,那是必然。

备注:通过 http://127.0.0.1:15672 来访问 Web 页面,默认情况下用户名和密码都是 guest,但生产环境下都应该改掉的。

  • rabbitmqctl 命令

与前端页面对应的就是后端的命令行命令了,同样非常丰富。平时自己测试,或者临时查看一些状态时,也能用得上


# 启动服务 
rabbitmq-server 

# 停止服务 
rabbitmqctl stop 

# vhost 增删查 
rabbitmqctl add_vhost 
rabbitmqctl delete_vhost 
rabbitmqctl list_vhosts 

# 查询交换机 
rabbitmqctl list_exchanges 

# 查询队列 
rabbitmqctl list_queues 

# 查看消费者信息 
rabbitmqctl list_consumers
 
# user 增删查 
rabbitmqctl add_user 
rabbitmqctl delete_user 
rabbitmqctl list_users
  • REST API

自动化监控和一些需要批量的操作,通过调用 API 来实现是最好的方式。比如有一些需要初始化的用户和权限,就可以通过脚本来一键完成,而不是通过页面逐个添加,简单又快捷。

下面是一些常用的 API:


# 概括信息
curl -i -u guest:guest http://localhost:15672/api/overview 

# vhost 列表 
curl -i -u guest:guest http://localhost:15672/api/vhosts 

# channel 列表 
curl -i -u guest:guest http://localhost:15672/api/channels 

# 节点信息 
curl -i -u guest:guest http://localhost:15672/api/nodes 

# 交换机信息 
curl -i -u guest:guest http://localhost:15672/api/exchanges 

# 队列信息 
curl -i -u guest:guest http://localhost:15672/api/queues 345678910111213141516

一般来讲 overview 和 queues 这两个 API 就可以满足需求。

下面是一个demo代码,主要使用HttpClient以及jackson来调用相关参数。

相关maven如下:

<dependency> 
    <groupId>org.apache.httpcomponents</groupId> 
    <artifactId>httpclient</artifactId>
    <version>4.3.6</version> 
</dependency> 
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId> 
    <version>2.7.4</version>
</dependency> 
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId> 
    <artifactId>jackson-annotations</artifactId> 
    <version>2.7.4</version> 
</dependency> 
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId> 
    <artifactId>jackson-core</artifactId> 
    <version>2.7.4</version>
</dependency>
public class MonitorDemo {
    public static void main(String[] args) {
        try {
            Map<String, ClusterStatus> map = fetchNodesStatusData("http://localhsot:15672/api/nodes", "root", "root");
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + " : " + entry.getValue());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Map<String, ClusterStatus> fetchNodesStatusData(String url, String username, String password) throws IOException {
        Map<String, ClusterStatus> clusterStatusMap = new HashMap<String, ClusterStatus>();
        String nodeData = getData(url, username, password);
        JsonNode jsonNode = null;
        try {
            jsonNode = JsonUtil.toJsonNode(nodeData);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Iterator<JsonNode> iterator = jsonNode.iterator();
        while (iterator.hasNext()) {
            JsonNode next = iterator.next();
            ClusterStatus status = new ClusterStatus();
            status.setDiskFree(next.get("disk_free").asLong());
            status.setFdUsed(next.get("fd_used").asLong());
            status.setMemoryUsed(next.get("mem_used").asLong());
            status.setProcUsed(next.get("proc_used").asLong());
            status.setSocketUsed(next.get("sockets_used").asLong());
            clusterStatusMap.put(next.get("name").asText(), status);
        }
        return clusterStatusMap;
    }

    public static String getData(String url, String username, String password) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, password);
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader(BasicScheme.authenticate(creds, "UTF-8", false));
        httpGet.setHeader("Content-Type", "application/json");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            if (response.getStatusLine().getStatusCode() != 200) {
                System.out.println("call http api to get rabbitmq data return code: " + response.getStatusLine().getStatusCode() + ", url: " + url);
            }
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                return EntityUtils.toString(entity);
            }
        } finally {
            response.close();
        }
        return "";
    }

    public static class JsonUtil {
        private static ObjectMapper objectMapper = new ObjectMapper();

        static {
            objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
            objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        }

        public static JsonNode toJsonNode(String jsonString) throws IOException {
            return objectMapper.readTree(jsonString);
        }
    }

    public static class ClusterStatus {
        private long diskFree;
        private long diskLimit;
        private long fdUsed;
        private long fdTotal;
        private long socketUsed;
        private long socketTotal;
        private long memoryUsed;
        private long memoryLimit;
        private long procUsed;
        private long procTotal; // 此处省略了Getter和Setter方法

        @Override
        public String toString() {
            return "ClusterStatus{" + "diskFree=" + diskFree + ", diskLimit=" + diskLimit + ", fdUsed=" + fdUsed + ", fdTotal="
                    + fdTotal + ", socketUsed=" + socketUsed + ", socketTotal=" + socketTotal + ", memoryUsed=" + memoryUsed
                    + ", memoryLimit=" + memoryLimit + ", procUsed=" + procUsed + ", procTotal=" + procTotal + '}';
        }
    }
}

通过对返回结果进行解析,就可以判断 RabbitMQ 的整体运行状态,如果发生超阈值的情况,可以发送告警或邮件,来达到监控的效果。

针对队列积压情况的监控判断,有两种方式:

  • 一是设置队列积压长度阈值,如果超过阈值即告警;

  • 二是保存最近五次的积压长度,如果积压逐渐增长并超阈值,即告警。

第二种方式更好,判断更加精准,误告可能性小,但实现起来也更复杂。

  • prometheus + grafana 监控rabbitmq

安装 docker-compose

前言

第一种:RabbitMQ内部集成Prometheus来获取指标

  • 3.8.0之前版本,RabbitMQ可以使用单独的插件prometheus_rabbitmq_exporter来向Prometheus公开指标,要单独下载到RabbitMQ安装目录中进行安装;

prometheus_rabbitmq_exporter:https://github.com/deadtrickster/prometheus_rabbitmq_exporter

  • 3.8.0版开始,RabbitMQ附带了内置的Prometheus&Grafana支持。虽然内置了该插件,但也要进行安装

rabbitmq-prometheus:https://github.com/rabbitmq/rabbitmq-prometheus

第二种:使用独立程序来获取指标(RabbitMQ_exporter)

不管什么版本都能使用,要单独启动exporter进程

rabbitmq_exporter:https://github.com/kbudde/rabbitmq_exporter

RabbitMQ 官方监控介绍:

https://www.rabbitmq.com/monitoring.html

https://www.rabbitmq.com/prometheus.html#overview-prometheus

先介绍采用第二种方式实现。(因为企业中使用的rabbitmq基本都是3.8.x之前的版本居多。)

采用docker方式启动

一、使用独立程序来获取指标(RabbitMQ_exporter)

安装rabbitmq_exporter

注:在RabbitMQ集群下的任意一个节点部署它。

  • 上传解压
wget https://github.com/kbudde/rabbitmq_exporter/releases/download/v0.26.0/rabbitmq_exporter-0.26.0.linux-amd64.tar.gz 

tar -xvf rabbitmq_exporter-0.26.0.linux-amd64.tar.gz 

cd rabbitmq_exporter-0.26.0.linux-amd64/
  • 配置

使用默认配置

  • 启动

进入根目录下,输入以下命令:

cd rabbitmq_exporter-0.26.0.linux-amd64/ 
RABBIT_USER=guest RABBIT_PASSWORD=guest OUTPUT_FORMAT=json PUBLIC_PORT=9090 RABBIT_URL=http://localhost:15672 nohup ./rabbitmq_exporter & tail -1000f nohup.out

参数说明:

RABBIT_USER:rabbit用户名
RABBIT_PASSWORD:rabbit密码
RABBIT_URL:rabbit服务地址和端口
OUTPUT_FORMAT:输出格式
PUBLIC_PORT:暴露端口

启动成功后,可以访问 http://10.0.101.100:9090/metrics/ ,(IP和端口要改成相应环境的)
看抓取的信息如下:

使用docker-compose方式启动prometheus和grafana

下载监控压缩包 rabbitmq-monitor.tar.gz,提取码:rppe

上传到服务器解压,参照上面的部署安装好docker和docker-compose,进入文件夹中执行:

docker-compose up -d 

#查看服务启动是否正常 
docker-compose ps

如果没用问题,就可以执行下面的步骤。

Prometheus配置

  • 配置

修改prometheus组件的prometheus.yml加入rabbitMQ节点:

vim prometheus.yml

  • 启动验证

用以下命令重启它,然后查看targets:

docker-compose restart prometheus

注:State=UP,说明成功

Grafana配置

导入仪表盘模板

通过浏览器访问:http://IP:3000

导入监控图表

以上仪表盘导入后再结合自身业务修改过的最终仪表盘:

通过浏览器访问:http://grafana服务器IP:3000

导入监控图表

以上仪表盘导入后再结合自身业务修改过的最终仪表盘:

  • 预警指标
序号 预警名称 预警规则 描述
1 集群状态预警 当集群状态不符合预期【!=1】时进行预警
2 节点状态预警 当节点状态不符合预期【!=1】时进行预警
3 等待消费预警 当等待消费的消息数量达到阈值【>1000】时进行预警 延迟消费
4 消费预警 当消费中的消息数量达到阈值【>1000】时进行预警 消费速度慢

二、RabbitMQ内部集成Prometheus来获取指标

官方文档

posted @ 2022-07-12 23:09  snail灬  阅读(1506)  评论(0编辑  收藏  举报