Zabbix5.0监控Elasitcsearch(Sender)

Zabbix5.0监控Elasitcsearch

1.什么是Elasticsearch

​ Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎,可以用于搜索各种文档。

​ Elasticsearch是与名为Logstash的数据收集和日志解析引擎以及名为Kibana的分析和可视化平台一起开发的。这三个产品被设计成一个集成解决方案,称为“Elastic Stack”(以前称为“ELK stack”)。

1.1 基础概念普及
1.1.1 集群cluster

​ 有多个节点产生,其中一个为主节点master,主节点是通过多个data节点选举出来的。

​ 候选master,需要设定一个属性node.master: true。

​ master节点的职责主要包括集群、节点和索引的管理。

1.1.2 索引分片shards

​ es可以把完整的索引分成多个分片,分布到不同的节点上,构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。

1.1.3 索引副本replicas

​ 提高容错性:es可以设置多个索引副本来提高系统容错性,当某个索引分片损坏或丢失,可以从副本中恢复。

​ 提高搜索性能:es会自动对搜索请求进行负载均衡。

1.1.4 数据重新分布recovery

​ es在有节点加入或退出时,根据机器的负载对索引分片进行重新分配。

​ 挂掉的节点重新启动,会进行数据恢复。

1.1.5 数据源插件river

​ 代表es的一个数据源,也是其它存储方式(如:数据库)同步数据到es的一个方法。

1.1.6 索引快照存储gateway

​ es默认是先把索引存放到内存中,当内存满了时再持久化到本地硬盘。gateway对索引快照进行存储,当这个es集群关闭再重新启动时就会从gateway中读取索引备份数据。

1.1.7 discovery.zen

​ es的自动发现节点机制,选主是ZenDiscovery模块负责的。

1.2 常见的问题
1.2.1 脑列问题

​ 如果master节点大于等于3:通过投票一半以上来解决。如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举。

如果master节点等于2:只能修改为唯一的一个master候选,其他作为data节点,避免脑裂问题。

1.2.2 GC问题

​ 由于es是由JAVA开发的,因此GC问题必须要考虑。

  • 倒排词典的索引需要常驻内存,无法GC,需要监控data node上segment memory增长趋势。
  • 各类缓存,field cache, filter cache, indexing cache, bulk queue等等,要设置合理的大小,并且要应该根据最坏的情况来看heap是否够用,也就是各类缓存全部占满的时候,还有heap空间可以分配给其他任务吗?
1.3 Elasticsearch重要监控指标

在这里插入图片描述

1.3.1 集群健康监控Cluster Health

​ 分片数的多少对集群性能有着很大影响。分片数量过多,则批量写入/查询请求被分割为过多的子写入/查询,导致该索引的写入、查询拒绝率上升; 对于数据量较大的索引,当分片数量过小时,无法充分利用节点资源,造成机器资源利用率不高或不均衡,影响写入/查询的效率。

#集群运行的重要指标:
#GET _cluster/health
Status:状态群集的状态。红色:部分主分片未分配。黄色:部分副本分片未分配。绿色:所有分片分配ok。
Nodes:节点。包括群集中的节点总数,并包括成功和失败节点的计数。 
active_primary_shards:主分片数。
Count of Active Shards:活动分片计数。集群中活动分片的数量。 
Relocating Shards:重定位分片。由于节点丢失而移动的分片计数。
Initializing Shards:初始化分片。由于添加索引而初始化的分片计数。 Unassigned
unassigned Shards。未分配的分片。尚未创建或分配副本的分片计数。
1.3.2 请求性能监控Request Performance

​ 当集群收到请求时,可能需要跨多个节点访问多个分片中的数据。系统处理和返回请求的速率、当前正在进行的请求数以及请求的持续时间等核心指标是衡量集群性能重要因素。

​ 请求过程本身分为两个阶段:

  • 第一是查询阶段(query phase),集群将请求分发到索引中的每个分片(主分片或副本分片)。
  • 第二个是获取阶段(fetch phrase),查询结果被收集,处理并返回给用户。
#请求检索性能的重要指标:
#index_a/_stats
query_current:当前正在进行的查询数。集群当前正在处理的查询计数。
fetch_current:当前正在进行的fetch次数。集群中正在进行的fetch计数。
query_total:查询总数。集群处理的所有查询的聚合数。
query_time_in_millis:查询总耗时。所有查询消耗的总时间(以毫秒为单位)。
fetch_total:提取总数。集群处理的所有fetch的聚合数。
fetch_time_in_millis:fetch所花费的总时间。所有fetch消耗的总时间(以毫秒为单位)。
1.3.3 索引性能监控Index Performance

​ 刷新(refresh)时间: 增、删、改操作,集群需要不断更新其索引,然后在所有节点上刷新它们。

​ 合并(Merge)时间:增、删、改批处理操作,会形成新段(segment)并刷新到磁盘,并且由于每个段消耗资源,因此将较小的段合并为更大的段,提高性能。

​ 这个两个操作,都是由集群本身管理。

#索引性能的重要指标:
#GET /_nodes/stats
refresh.total:总刷新计数。刷新总数的计数。
refresh.total_time_in_millis:刷新总时间。汇总所有花在刷新的时间(以毫秒为单位进行测量)。
merges.current_docs:目前的合并。合并目前正在处理中。
merges.total_docs:合并总数。合并总数的计数。
merges.total_stopped_time_in_millis。合并花费的总时间。合并段的所有时间的聚合。
1.3.4 系统健康监控System Health

​ 内存指标:Elasticsearch是一个严重依赖内存 以实现性能的系统,因此密切关注内存使用情况与每个节点的运行状况和性能相关。

​ 磁盘I/O:访问磁盘在非常花费时间的,磁盘高读写可能会导致系统性能问题。

​ CPU使用情况:有助于识别节点中的低效进程或潜在问题。

#系统运行重要指标:
#GET /_cat/nodes
disk.total :总磁盘容量。节点主机上的总磁盘容量。
disk.used:总磁盘使用量。节点主机上的磁盘使用总量。
avail disk:可用磁盘空间总量。
disk.avail disk.used_percent:使用的磁盘百分比。已使用的磁盘百分比。
ram:当前的RAM使用情况。当前内存使用量(测量单位)。
percent ram:RAM百分比。正在使用的内存百分比。
max : 最大RAM。 节点主机上的内存总量
cpu:中央处理器。正在使用的CPU百分比。
1.3.5 JVM健康监控JVM Health

​ 作为基于Java的应用程序,Elasticsearch在Java虚拟机(JVM)中运行。JVM在其“堆”分配中管理其内存,并通过garbage collection进行垃圾回收处理。JVM内存分配给不同的内存池。需要密切注意这些池中的每个池。

​ 如果应用程序的需求超过堆的容量,则应用程序开始强制使用连接的存储介质上的交换空间。虽然这可以防止系统崩溃,但它可能会对集群的性能造成严重破坏。

#JVM运行状况重要指标:
#GET /_nodes/stats 
mem:内存使用情况。堆和非堆进程和池的使用情况统计信息。
threads:当前使用的线程和最大数量。
gc:垃圾收集。垃圾收集所花费的总时间。

2.如何监控Elasticsearch

2.1 采集监控数据

采集集群健康状态(格式化): http://esurl:9200/_cluster/health?pretty

采集集群健康状态(列名): http://esurl:9200/_cluster/health?v

2.1.1 shell脚本采集
[root@localhost ~]# cat  /etc/zabbix/script/zabbix_elasticsearch.sh

elasticsearch_ip='127.0.0.1'
elasticsearch_port='9200'
elasticsearch_uri="http://${elasticsearch_ip}:${elasticsearch_port}"

#!/bin/bash
case $1 in
    cluster_name)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F" '/cluster_name/ {print $4}' ;;
    status)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F" 'NR==3 {print $4}' ;;
    timed_out)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==4 {print $1}' |awk -F: '{print $2}' ;;
    number_nodes) #Node节点数
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==5 {print $1}' |awk -F: '{print $2}' ;;
    data_nodes)  #数据节点数
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==6 {print $1}' |awk -F: '{print $2}' ;;
    active_primary_shards)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==7 {print $1}' |awk -F: '{print $2}' ;;
    active_shards)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==8 {print $1}' |awk -F: '{print $2}' ;;
    relocating_shards)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==9 {print $1}' |awk -F: '{print $2}' ;;
    initializing_shards)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==10 {print $1}' |awk -F: '{print $2}' ;;
    unassigned_shards)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==11 {print $1}' |awk -F: '{print $2}' ;;
    delayed_unassigned_shards)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==12 {print $1}' |awk -F: '{print $2}' ;;
    number_of_pending_tasks)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==13 {print $1}' |awk -F: '{print $2}' ;;
    active_shards_percent_as_number)
        curl -s -XGET '{elasticsearch_uri}/_cluster/health?pretty' |awk -F, 'NR==16 {print $1}' |awk -F: '{print $2}' ;;
    *)
 
        echo "Usage: $0 { cluster_name | status | timed_out | number_nodes | data_nodes | active_primary_shards | active_shards | relocating_shards | initializing_shards | unassigned_shards|delayed_unassigned_shards|number_of_pending_tasks|active_shards_percent_as_number}" ;;
esac

在shell脚本里,“curl -s -XGET ‘http://{elasticsearch_uri}/_cluster/health?pretty’ |awk -F, ‘NR==16 {print $1}’ |awk -F: ‘{print $2}’’”这样的命令,“NR==16”是指在浏览器访问{elasticsearch_uri}/_cluster/health?pretty,获取页面的第16行(从第1行的“{”开始计数)。

注:shell脚本的配置方法用zabbix的自定义key采集,大家可以发现,一个Url能查出很多个指标,这种编程模式的缺点就是会重复调用同一个Url提取不同的指标,这种做法不是最优的。

如何优化:我们希望就是调用一个Url的时候,就直接把能查到的指标一次全保存下来,最后统一发送,减少与elasticsearch之间的交互次数。

因此我们决定采用Sender方法,脚本逻辑改为用python脚本。

2.1.2 python脚本采集
#!/usr/bin/env python
#coding:utf-8

from __future__ import division   
import json,requests,sys,os
import pickle


local_ip= '166.8.30.23'

#设置一个默认端口9200,这个后期使用自动发现比较好
local_port = 9200


def discovery(local_ip,local_port):
    r = {}
    r['data'] = []

    res = requests.get("http://{0}:{1}/_cat/nodes?v&h=name".format(local_ip,local_port))
    if res.status_code == 200:
        ret = res.text.splitlines()
        for i in range(1,len(ret)):
            r['data'].append({'{#NODE}':ret[i]})
    return json.dumps(r)


def send(local_ip,local_port):
    r_str = ""
    zbx_sender_cmd = "{0} -c {1} -i {2}"
    zbx_conf = "/usr/local/zabbix_proxy/etc/zabbix_agentd.conf"
    zbx_sender_file = "/tmp/.zbx_elastic_sender.txt"
    zbx_sender = "/usr/local/zabbix_proxy/bin/zabbix_sender"
    last_file = "/tmp/.zbx_elstic_last_data.txt"

    #需要pickle保存到文件中的字典
    this_data = {}
    
    #导入历史数据
    if os.path.exists(last_file) :
        if os.path.getsize(last_file) != 0:
            with open(last_file,"rb") as f1:
                last_data = pickle.load(f1)
        else:
            last_data = {}
    else:
        os.popen("touch {0}".format(last_file))
        last_data = {}
    #print(last_data)

    # 获取node信息的url
    url_node = "http://{0}:{1}/_nodes/stats?pretty".format(local_ip, local_port)
    res_node = requests.get(url_node)
    if res_node.status_code == 200:
        ret_node = res_node.json()
        for node,node_value in ret_node["nodes"].items():
            #print(ret_node["nodes"][node]["name"])  #打印出节点名字
            node_name = node_value["name"]
            this_data[node_name] = {}

            #需要获取docs,segments,get,search,merges,flush,warmer等信息
            #docs,segments
            r_str += "- elastic.indices.docs.count.[{0}] {1}\n".format(node_name,node_value["indices"]["docs"]["count"])
            r_str += "- elastic.indices.docs.deleted.[{0}] {1}\n".format(node_name, node_value["indices"]["docs"]["deleted"])
            r_str += "- elastic.indices.segments.count.[{0}] {1}\n".format(node_name, node_value["indices"]["segments"]["count"])
            r_str += "- elastic.indices.segments.memory.[{0}] {1}\n".format(node_name,node_value["indices"]["segments"]["memory_in_bytes"])

            #indexing,get,search
            indexing_num = node_value["indices"]["indexing"]["index_total"]
            indexing_time = node_value["indices"]["indexing"]["index_time_in_millis"]/1000
            r_str += "- elastic.indices.indexing.total.[{0}] {1}\n".format(node_name, indexing_num)
            this_data[node_name].update({"indexing_num": indexing_num})
            r_str += "- elastic.indices.indexing.time.[{0}] {1}\n".format(node_name, indexing_time)
            this_data[node_name].update({"indexing_time": indexing_time})
            if node_name not in last_data.keys() or (indexing_num - last_data[node_name]["indexing_num"]) == 0:
                r_str += "- elastic.indices.indexing.per_time.[{0}] {1}\n".format(node_name,0)
            else:
                r_str += "- elastic.indices.indexing.per_time.[{0}] {1}\n".format(node_name,
                        round((indexing_time - last_data[node_name]["indexing_time"])/(indexing_num - last_data[node_name]["indexing_num"]),3)
                                                                          )

            get_num = node_value["indices"]["get"]["total"]
            get_time = node_value["indices"]["get"]["time_in_millis"]/1000
            r_str += "- elastic.indices.get.total.[{0}] {1}\n".format(node_name, get_num)
            this_data[node_name].update({"get_num": get_num})
            r_str += "- elastic.indices.get.time.[{0}] {1}\n".format(node_name, get_time)
            this_data[node_name].update({"get_time": get_time})
            if node_name not in last_data.keys() or (get_num - last_data[node_name]["get_num"]) == 0:
                r_str += "- elastic.indices.get.per_time.[{0}] {1}\n".format(node_name,0)
            else:
                r_str += "- elastic.indices.get.per_time.[{0}] {1}\n".format(node_name,
                        round((get_time - last_data[node_name]["get_time"])/(get_num - last_data[node_name]["get_num"]),3)
                                                                     )

            query_num = node_value["indices"]["search"]["query_total"]
            query_time = node_value["indices"]["search"]["query_time_in_millis"]/1000
            r_str += "- elastic.indices.search.query_total.[{0}] {1}\n".format(node_name, query_num)
            this_data[node_name].update({"query_num": query_num})
            r_str += "- elastic.indices.search.query_time.[{0}] {1}\n".format(node_name, query_time)
            this_data[node_name].update({"query_time": query_time})
            if node_name not in last_data.keys() or (query_num - last_data[node_name]["query_num"]) == 0:
                r_str += "- elastic.indices.query.per_time.[{0}] {1}\n".format(node_name,0)
            else:
                r_str += "- elastic.indices.query.per_time.[{0}] {1}\n".format(node_name,
                        round((query_time - last_data[node_name]["query_time"])/(query_num - last_data[node_name]["query_num"]),3)
                                                                         )

            fetch_num = node_value["indices"]["search"]["fetch_total"]
            fetch_time = node_value["indices"]["search"]["fetch_time_in_millis"]/1000
            r_str += "- elastic.indices.search.fetch_total.[{0}] {1}\n".format(node_name, fetch_num)
            this_data[node_name].update({"fetch_num": fetch_num})
            r_str += "- elastic.indices.search.fetch_time.[{0}] {1}\n".format(node_name,fetch_time)
            this_data[node_name].update({"fetch_time": fetch_time})
            if node_name not in last_data.keys() or (fetch_num - last_data[node_name]["fetch_num"]) == 0:
                r_str += "- elastic.indices.fetch.per_time.[{0}] {1}\n".format(node_name,0)
            else:
                r_str += "- elastic.indices.fetch.per_time.[{0}] {1}\n".format(node_name,
                        round((fetch_time - last_data[node_name]["fetch_time"])/(fetch_num - last_data[node_name]["fetch_num"]),3)
                                                                         )

            #merges,refresh,flush,warmer
            for oper in ["merges","refresh","flush","warmer"]:
                #这里有点容易混淆
                p_data = {}
                str_num = "{0}_num".format(oper)
                str_time = "{0}_time".format(oper)
                p_data[str_num] = node_value["indices"][oper]["total"]
                p_data[str_time] = node_value["indices"][oper]["total_time_in_millis"]/1000

                r_str += "- elastic.indices.{0}.total.[{1}] {2}\n".format(oper,node_name, p_data[str_num])
                this_data[node_name].update({"{0}_num".format(oper): p_data[str_num]})
                r_str += "- elastic.indices.{0}.time.[{1}] {2}\n".format(oper,node_name, p_data[str_time])
                this_data[node_name].update({"{0}_time".format(oper): p_data[str_time]})
                if node_name not in last_data.keys() or (p_data[str_num] - last_data[node_name][str_num]) == 0:
                    r_str += "- elastic.indices.{0}.per_time.[{1}] {2}\n".format(oper,node_name, 0)
                else:
                    r_str += "- elastic.indices.{0}.per_time.[{1}] {2}\n".format(oper,node_name,
                                round((p_data[str_time] - last_data[node_name][str_time]) / (p_data[str_num] - last_data[node_name][str_num]),3)
                                                                               )

            #jvm基本
            r_str += "- elastic.jvm.heap_max_in_bytes.[{0}] {1}\n".format(node_name,node_value["jvm"]["mem"]["heap_max_in_bytes"])
            r_str += "- elastic.jvm.heap_used_in_bytes.[{0}] {1}\n".format(node_name,node_value["jvm"]["mem"]["heap_used_in_bytes"])
            r_str += "- elastic.jvm.threads.[{0}] {1}\n".format(node_name,node_value["jvm"]["threads"]["count"])
            r_str += "- elastic.jvm.buffer_pools.used_in_bytes.[{0}] {1}\n".format(node_name,node_value["jvm"]["buffer_pools"]["direct"]["used_in_bytes"])
            r_str += "- elastic.jvm.buffer_pools.total_capacity_in_bytes.[{0}] {1}\n".format(node_name,node_value["jvm"]["buffer_pools"]["direct"]["total_capacity_in_bytes"])

            #jvm垃圾回收的两个总量,要用减法。后面改进要计算出平均垃圾回收的时间
            r_str += "- elastic.jvm.gc.young.num.[{0}] {1}\n".format(node_name,node_value["jvm"]["gc"]["collectors"]["young"]["collection_count"])
            r_str += "- elastic.jvm.gc.young.time.[{0}] {1}\n".format(node_name,node_value["jvm"]["gc"]["collectors"]["young"]["collection_time_in_millis"]/1000)
            r_str += "- elastic.jvm.gc.old.num.[{0}] {1}\n".format(node_name,node_value["jvm"]["gc"]["collectors"]["old"]["collection_count"])
            r_str += "- elastic.jvm.gc.old.time.[{0}] {1}\n".format(node_name,node_value["jvm"]["gc"]["collectors"]["old"]["collection_time_in_millis"]/1000)

            for m,n in node_value["jvm"]["mem"]["pools"].items():
                r_str += "- elastic.jvm.{0}.used_in_bytes.[{1}] {2}\n".format(m,node_name,n["used_in_bytes"])
                r_str += "- elastic.jvm.{0}.max_in_bytes.[{1}] {2}\n".format(m,node_name,n["max_in_bytes"])

            #thread_pool,这个东西正常情况下没什么,有些时候还是能发现很多问题的
            for k,v in node_value["thread_pool"].items():
                r_str += "- elastic.thread_pool.{0}.threads.[{1}] {2}\n".format(k,node_name,v[u"threads"])
                r_str += "- elastic.thread_pool.{0}.threads.queue.[{1}] {2}\n".format(k,node_name,v[u"queue"])

            #http和script,其中script是总的数据,做减法
            r_str += "- elastic.http.current_open.[{0}] {1}\n".format(node_name,node_value["http"]["current_open"])
            r_str += "- elastic.script.compilations.[{0}] {1}\n".format(node_name,node_value["script"]["compilations"])

            #处理每种操作的平均时间time差值/total差值
            #print(this_data)
            with open(last_file,"wb") as f2:
                pickle.dump(this_data,f2)



    else:
        sys.stderr.write("Fetch node info error!")
    
    # 获取集群信息的url
    url_cluster = "http://{0}:{1}/_cluster/health".format(local_ip, local_port)
    res_cluster = requests.get(url_cluster)
    if res_cluster.status_code == 200:
        ret_cluster = res_cluster.json()

        #print(ret_cluster)
        #绿是0,黄是1,红是2
        if ret_cluster[u'status'] == u"green":
            status = 0
        elif ret_cluster[u'status'] == u"yellow":
            status = 1
        else:
            status = 2

        r_str +=  "- elastic.cluster.status {0}\n".format(status)
        r_str += "- elastic.cluster.non {0}\n".format(ret_cluster[u"number_of_nodes"])
        r_str += "- elastic.cluster.us {0}\n".format(ret_cluster[u"unassigned_shards"])
        r_str += "- elastic.cluster.nopt {0}\n".format(ret_cluster[u"number_of_pending_tasks"])
        r_str += "- elastic.cluster.noiff {0}\n".format(ret_cluster[u"number_of_in_flight_fetch"])
        r_str += "- elastic.cluster.aps {0}\n".format(ret_cluster[u"active_primary_shards"])
        r_str += "- elastic.cluster.tmwiqm {0}\n".format(ret_cluster[u"task_max_waiting_in_queue_millis"])
        r_str += "- elastic.cluster.rs {0}\n".format(ret_cluster[u"relocating_shards"])
        r_str += "- elastic.cluster.aspan {0}\n".format(ret_cluster[u"active_shards_percent_as_number"])
        r_str += "- elastic.cluster.as {0}\n".format(ret_cluster[u"active_shards"])
        r_str += "- elastic.cluster.is {0}\n".format(ret_cluster[u"initializing_shards"])
        r_str += "- elastic.cluster.dus {0}\n".format(ret_cluster[u"delayed_unassigned_shards"])
        r_str += "- elastic.cluster.nodn {0}\n".format(ret_cluster[u"number_of_data_nodes"])
    else:
        sys.stderr.write("Fetch node info error!")
    #print(r_str)
    
    with open(zbx_sender_file,"w") as f:
        f.write(r_str)


    send_ret = os.popen(zbx_sender_cmd.format(zbx_sender, zbx_conf, zbx_sender_file))
    print(zbx_sender_cmd.format(zbx_sender, zbx_conf, zbx_sender_file))
    if "failed: 0" in send_ret.read():  #这一步,用一个普通的item来触发,并返回执行结果,1是正常的,0是发送异常
        print(1)
    else:
        print(0)
    


if __name__ == "__main__":
    if len(sys.argv) == 2 and sys.argv[1]=="discovery":
        ret = discovery(local_ip,local_port)
        print(ret)
    elif len(sys.argv) == 1:
        send(local_ip,local_port)
    else:
        sys.stderr.write("Args is wrong!")

本文参考如下:

【Elasticsearch Top10 监控指标】 https://cloud.tencent.com/developer/article/1427221

【zabbix监控elasticsearch集群】https://blog.csdn.net/jiangmingfei/article/details/87089997

感谢两位的分享,通过研究和实践,最终确定了监控es的方法,因此把实践总结分享给各位。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值