作者丨 caiyfc
来自神州数码钛合金战队
神州数码钛合金战队是一支致力于为企业提供分布式数据库 TiDB 整体解决方案的业余技术团队。团队成员领有丰盛的数据库从业背景,全副领有 TiDB 高级资格证书,并沉闷于 TiDB 开源社区,是官网认证合作伙伴。目前已为 10+ 客户提供了业余的 TiDB 交付服务,涵盖金融、证券、物流、电力、政府、批发等重点行业。
背景
笔者最近在驻场,发现这里的 tidb 集群是真的多,有将近 150 套集群。而且集群少则 6 个节点起步,多则有 200 多个节点。在这么宏大的集群体量下,巡检就变得十分的繁琐了。
那么有没有什么方法可能代替手动巡检,并且可能疾速精确的获取到集群相干信息的办法呢?答案是,有但不齐全有。其实能够利用 tidb 的 Prometheus 来获取集群相干的各项数据,比方告警就是一个很好的例子。惋惜了,告警只是获取了以后数据进行告警判断,而巡检须要应用一段时间的数据来作为判断的根据。而且,告警是曾经达到临界值了,巡检却是要排查集群的隐患,提前开始布局,防止出现异常。
那间接用 Prometheus 获取一段时间的数据,并且把告警值改低不就行了?
意识 PromQL
要应用 Prometheus ,那必须要先理解什么是 PromQL 。
PromQL 查询语言和日常应用的数据库 SQL 查询语言(SELECT * FROM ...)是不同的,PromQL 是一 种 嵌套的函数式语言 ,就是咱们要把需 要查找的数据形容成一组嵌套的表达式,每个表达式都会评估为一个两头值,每个两头值都会被用作它下层表达式中的参数,而查问的最外层表达式示意你能够在表格、图形中看到的最终返回值。比方上面的查问语句:
histogram_quantile( # 查问的根,最终后果示意一个近似分位数。 0.9, # histogram_quantile() 的第一个参数,分位数的目标值 # histogram_quantile() 的第二个参数,聚合的直方图 sum by(le, method, path) ( # sum() 的参数,直方图过来5分钟每秒增量。 rate( # rate() 的参数,过来5分钟的原始直方图序列 demo_api_request_duration_seconds_bucket{job="demo"}[5m] ) ))
而后还须要认识一下告警的 PromQL 中,经常出现的一些函数:
rate
用于计算变化率的最常见 函数是 rate() , rate() 函数用于计算在指定工夫范畴内计数器均匀每秒的增加量。因为是计算一个工夫范畴内的平均值,所以咱们须要在序列选择器之后增加一个范畴选择器。
irate
因为应用 rate 或者 increase 函数去计算样本的均匀增长速率,容易陷入长尾问题当中,其无奈反馈在工夫窗口内样本数据的突发变动。
例如,对于主机而言在 2 分钟的工夫窗口内,可能在某一个因为访问量或者其它问题导致 CPU 占用 100%的状况,然而通过计算在工夫窗口内的均匀增长率却无奈反馈出该问题。
为了解决该问题,PromQL 提供了另外一个灵敏度更高 的函数 irate(v range-vector) 。 irate 同样用于计算区间向量的计算率,然而其 反馈出的是刹时增长率。
histogram_quantile
获取数据的分位数。histogram_quantile( scalar, b instant-vector) 函数用于计算历史数据指标一段时间内的分位数。该函数将指标分位数 (0 ≤ ≤ 1) 和直方图指标作为输出,就是大家平时讲的 pxx,p50 就是中位数,参数 b 肯定是蕴含 le 这个标签的刹时向量,不蕴含就无从计算分位数了,然而计算的分位数是一个预估值,并不齐全精确,因为这个函数是假设每个区间内的样本分布是线性散布来计算结果值的,预估的准确度取决于 bucket 区间划分的粒度,粒度越大,准确度越低。
该局部援用: Prometheus 根底相干--PromQL 根底(2) ( https://zhuanlan.zhihu.com/p/586528847 ) 想学习的同学能够去看看原文
批改 PromQL
要让巡检应用 PromQL ,就必须要批改告警中的 PromQL。这里须要介绍一个函数:max_over_time(range-vector),它是获取区间向量内每个指标的最大值。其实还有其余这类工夫聚合函数,比方 avg_over_time、min_over_time、sum_over_time 等等,然而咱们只须要获取到最大值,来揭示 dba 就行了。
Prometheus 是反对子查问的,它容许咱们首先以指定的步长在一段时间内执行外部查问,而后依据子查问的后果计算内部查问。子查问的示意形式相似于区间向量的持续时间,但须要冒号后增加了一个额定的步长参数: [:]。
举个例子:
# 原版sum(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance)# 批改max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance)[24h:1m])
这是获取 TiKV raftstore 线程池 CPU 使用率的告警项。原版是间接将 1 分钟内所有线程的变化率相加,而笔者的修改版是将 1 分钟内所有线程的使用率取平均值,并且从此刻向后倒 24 小时内,每一分钟执行一次获取均匀线程使用率的查问,再取最大值。
也就是说,从 24 小时前,到当初,每分钟执行一次(步长为 1 分钟): avg(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance) ,并获取其中最大的一次值。这样就满足了咱们须要应用一段时间的数据来判断集群是否有危险的根据了。
而后咱们能够选取适合的 PromQL 来加上工夫聚合函数和查问工夫及步长信息:
# TiKV 1'TiDB.tikv.TiKV_server_is_down': { 'pql': 'probe_success{group="tikv",instance=~".*"} == 0', 'pql_max': '', 'note': 'TiKV 服务不可用'},'TiDB.tikv.TiKV_node_restart': { 'pql': 'changes(process_start_time_seconds{job="tikv",instance=~".*"}[24h])> 0', 'pql_max': 'max(changes(process_start_time_seconds{job="tikv",instance=~".*"}[24h]))', 'note': 'TiKV 服务5分钟内呈现重启'},'TiDB.tikv.TiKV_GC_can_not_work': { 'pql_max': '', 'pql': 'sum(increase(tikv_gcworker_gc_tasks_vec{task="gc", instance=~".*"}[2d])) by (instance) < 1 and (sum(increase(' 'tikv_gc_compaction_filter_perform{instance=~".*"}[2d])) by (instance) < 1 and sum(increase(' 'tikv_engine_event_total{cf="write",db="kv",type="compaction",instance=~".*"}[2d])) by (instance) >= 1)', 'note': 'TiKV 服务GC无奈工作'},# TiKV 2'TiDB.tikv.TiKV_raftstore_thread_cpu_seconds_total': { 'pql_max': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance)[24h:1m])', 'pql': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance)[24h:1m]) > 0.8', 'note': 'TiKV raftstore 线程池 CPU 使用率过高'},'TiDB.tikv.TiKV_approximate_region_size': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_region_size_bucket{instance=~".*"}[1m])) ' 'by (le,instance))[24h:1m])', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_region_size_bucket{instance=~".*"}[1m])) ' 'by (le,instance))[24h:1m]) > 1073741824', 'note': 'TiKV split checker 扫描到的最大的 Region approximate size 大于 1 GB'},'TiDB.tikv.TiKV_async_request_write_duration_seconds': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket' '{type="write", instance=~".*"}[1m])) by (le, instance, type))[24h:1m])', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket' '{type="write", instance=~".*"}[1m])) by (le, instance, type))[24h:1m]) > 1', 'note': 'TiKV 中Raft写入响应工夫过长'},'TiDB.tikv.TiKV_scheduler_command_duration_seconds': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_scheduler_command_duration_seconds_bucket[20m])) by (le, instance, type) / 1000)[24h:20m]) ', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_scheduler_command_duration_seconds_bucket[20m])) by (le, instance, type) / 1000)[24h:20m]) > 20 ', 'note': 'TiKV 调度器申请响应工夫过长'},'TiDB.tikv.TiKV_scheduler_latch_wait_duration_seconds': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_scheduler_latch_wait_duration_seconds_bucket[20m])) by (le, instance, type))[24h:20m]) ', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_scheduler_latch_wait_duration_seconds_bucket[20m])) by (le, instance, type))[24h:20m]) > 20', 'note': 'TiKV 调度器锁期待响应工夫过长'},'TiDB.tikv.TiKV_write_stall': { 'pql_max': 'max_over_time(delta(tikv_engine_write_stall{instance=~".*"}[10m])[24h:10m])', 'pql': 'max_over_time(delta(' 'tikv_engine_write_stall{instance=~".*"}[10m])[24h:10m]) > 10', 'note': 'TiKV 中存在写入积压'},# TiKV 3'TiDB.tikv.TiKV_server_report_failure_msg_total': { 'pql_max': 'max_over_time(sum(rate(tikv_server_report_failure_msg_total{type="unreachable"}[10m])) BY (instance)[24h:10m])', 'pql': 'max_over_time(sum(rate(tikv_server_report_failure_msg_total{type="unreachable"}[10m])) BY (instance)[24h:10m]) > 10', 'note': 'TiKV 节点报告失败次数过多'},'TiDB.tikv.TiKV_channel_full_total': { 'pql_max': 'max_over_time(sum(rate(tikv_channel_full_total{instance=~".*"}[10m])) BY (type, instance)[24h:10m])', 'pql': 'max_over_time(sum(rate(tikv_channel_full_total{instance=~".*"}[10m])) BY (type, instance)[24h:10m]) > 0', 'note': 'TIKV 通道已占满 tikv 过忙'},'TiDB.tikv.TiKV_raft_log_lag': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_log_lag_bucket{instance=~".*"}[1m])) by (le,instance))[24h:10m])', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_log_lag_bucket{instance=~".*"}[1m])) by (le, ' 'instance))[24h:10m]) > 5000', 'note': 'TiKV 中 raft 日志同步相差过大'},'TiDB.tikv.TiKV_thread_unified_readpool_cpu_seconds': { 'pql_max': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"unified_read_po*", instance=~".*"}[1m])) by (instance)[24h:1m])', 'pql': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"unified_read_po*", instance=~".*"}[1m])) ' 'by (instance)[24h:1m]) > 0.7', 'note': 'unifiled read 线程池使用率大于70%'},'TiDB.tikv.TiKV_low_space': { 'pql_max': 'sum(tikv_store_size_bytes{type="available"}) by (instance) / sum(tikv_store_size_bytes{type="capacity"}) by (instance)', 'pql': 'sum(tikv_store_size_bytes{type="available"}) by (instance) / sum(tikv_store_size_bytes{type="capacity"}) by (instance) < 0.3', 'note': 'TiKV 以后存储可用空间小于阈值'},
因为有的告警项是获取了 5 分钟或者 10 分钟的数据,在写步长的时候也要同步批改为 5 分钟或者 10 分钟,保持一致能够保障,查看能笼罩选定的全副时间段,并且不会反复计算造成资源节约。
顺带一提,如果不加 max_over_time 能够获取到带有工夫戳的全副数据,而不是只获取到最大的一个数据。这个带工夫戳的全副数据能够不便画图,像 grafana 那样展现数据趋势。
巡检脚本
理解了以上所有常识,咱们就能够开始编写巡检脚本了。
这是笔者和共事独特编写的一部分巡检脚本,最重要的是 tasks 中的 PromQL ,在脚本执行之前要写好 PromQL,其余局部能够随便更改。如果一次性巡检天数太多,比方一次巡检一个月的工夫,Prometheus 可能会因检查数据太多而报错的,所以应用的时候要留神报错信息,防止漏掉一些巡检项。
# -*- coding: utf-8 -*-import subprocessimport reimport datetimeimport requestsimport sysimport pandas as pddays = Nonedef get_cluster_name(): try: command = "tiup cluster list" result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, error = result.communicate() cluster_name_match = re.search(r'([a-zA-Z0-9_-]+)\s+tidb\s+v', output.decode('utf-8')) if cluster_name_match: return cluster_name_match.group(1) else: return None except Exception as e: print("An error occurred:", e) return Nonedef display_cluster_info(cluster_name): if not cluster_name: print("Cluster name not found.") return try: command = "tiup cluster display {0}".format(cluster_name) result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, error = result.communicate() return output.decode('utf-8') except Exception as e: print("An error occurred:", e)def extract_id_role(output): id_role_dict = {} lines = output.strip().split("\n") for line in lines: print(line) parts = line.split() if is_valid_ip_port(parts[0]): node_id, role = parts[0], parts[1] id_role_dict[node_id] = role return id_role_dictdef is_valid_ip_port(input_str): pattern = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$') return bool(pattern.match(input_str))def get_prometheus_ip(data_dict): prometheus_ip = None for key, value in data_dict.items(): if value == 'prometheus': prometheus_ip = key break return prometheus_ipdef get_tasks(): global days tasks = { # TiKV 1 'TiDB.tikv.TiKV_server_is_down': { 'pql': 'probe_success{group="tikv",instance=~".*"} == 0', 'pql_max': '', 'note': 'TiKV 服务不可用' }, 'TiDB.tikv.TiKV_node_restart': { 'pql': 'changes(process_start_time_seconds{job="tikv",instance=~".*"}[24h])> 0', 'pql_max': 'max(changes(process_start_time_seconds{job="tikv",instance=~".*"}[24h]))', 'note': 'TiKV 服务5分钟内呈现重启' }, 'TiDB.tikv.TiKV_GC_can_not_work': { 'pql_max': '', 'pql': 'sum(increase(tikv_gcworker_gc_tasks_vec{task="gc", instance=~".*"}[2d])) by (instance) < 1 and (sum(increase(' 'tikv_gc_compaction_filter_perform{instance=~".*"}[2d])) by (instance) < 1 and sum(increase(' 'tikv_engine_event_total{cf="write",db="kv",type="compaction",instance=~".*"}[2d])) by (instance) >= 1)', 'note': 'TiKV 服务GC无奈工作' }, # TiKV 2 'TiDB.tikv.TiKV_raftstore_thread_cpu_seconds_total': { 'pql_max': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance)[24h:1m])', 'pql': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"(raftstore|rs)_.*"}[1m])) by (instance)[24h:1m]) > 0.8', 'note': 'TiKV raftstore 线程池 CPU 使用率过高' }, 'TiDB.tikv.TiKV_approximate_region_size': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_region_size_bucket{instance=~".*"}[1m])) ' 'by (le,instance))[24h:1m])', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_region_size_bucket{instance=~".*"}[1m])) ' 'by (le,instance))[24h:1m]) > 1073741824', 'note': 'TiKV split checker 扫描到的最大的 Region approximate size 大于 1 GB' }, 'TiDB.tikv.TiKV_async_request_write_duration_seconds': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket' '{type="write", instance=~".*"}[1m])) by (le, instance, type))[24h:1m])', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket' '{type="write", instance=~".*"}[1m])) by (le, instance, type))[24h:1m]) > 1', 'note': 'TiKV 中Raft写入响应工夫过长' }, 'TiDB.tikv.TiKV_write_stall': { 'pql_max': 'max_over_time(delta(tikv_engine_write_stall{instance=~".*"}[10m])[24h:10m])', 'pql': 'max_over_time(delta(' 'tikv_engine_write_stall{instance=~".*"}[10m])[24h:10m]) > 10', 'note': 'TiKV 中存在写入积压' }, # TiKV 3 'TiDB.tikv.TiKV_server_report_failure_msg_total': { 'pql_max': 'max_over_time(sum(rate(tikv_server_report_failure_msg_total{type="unreachable"}[10m])) BY (instance)[24h:10m])', 'pql': 'max_over_time(sum(rate(tikv_server_report_failure_msg_total{type="unreachable"}[10m])) BY (instance)[24h:10m]) > 10', 'note': 'TiKV 节点报告失败次数过多' }, 'TiDB.tikv.TiKV_channel_full_total': { 'pql_max': 'max_over_time(sum(rate(tikv_channel_full_total{instance=~".*"}[10m])) BY (type, instance)[24h:10m])', 'pql': 'max_over_time(sum(rate(tikv_channel_full_total{instance=~".*"}[10m])) BY (type, instance)[24h:10m]) > 0', 'note': 'TIKV 通道已占满 tikv 过忙' }, 'TiDB.tikv.TiKV_raft_log_lag': { 'pql_max': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_log_lag_bucket{instance=~".*"}[1m])) by (le,instance))[24h:10m])', 'pql': 'max_over_time(histogram_quantile(0.99, sum(rate(tikv_raftstore_log_lag_bucket{instance=~".*"}[1m])) by (le, ' 'instance))[24h:10m]) > 5000', 'note': 'TiKV 中 raft 日志同步相差过大' }, 'TiDB.tikv.TiKV_thread_unified_readpool_cpu_seconds': { 'pql_max': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"unified_read_po*", instance=~".*"}[1m])) by (instance)[24h:1m])', 'pql': 'max_over_time(avg(rate(tikv_thread_cpu_seconds_total{name=~"unified_read_po*", instance=~".*"}[1m])) ' 'by (instance)[24h:1m]) > 0.7', 'note': 'unifiled read 线程池使用率大于70%' }, 'TiDB.tikv.TiKV_low_space': { 'pql_max': 'sum(tikv_store_size_bytes{type="available"}) by (instance) / sum(tikv_store_size_bytes{type="capacity"}) by (instance)', 'pql': 'sum(tikv_store_size_bytes{type="available"}) by (instance) / sum(tikv_store_size_bytes{type="capacity"}) by (instance) < 0.3', 'note': 'TiKV 以后存储可用空间小于阈值' }, } for key, value in tasks.items(): for inner_key, inner_value in value.items(): if isinstance(inner_value, str) and 'pql' in inner_key: value[inner_key] = inner_value.replace("24h:", f"{24 * days}h:").replace("[24h]", f"[{24 * days}h]") return tasksdef request_prome(prometheus_address, query): try: response = requests.get('http://%s/api/v1/query' % prometheus_address, params={'query': query}) return response except: return Nonedef has_response(prometheus_address, query): response = request_prome(prometheus_address, query) if not response: return False try: if response.json()["data"]['result']: return True else: return False except: return Falsedef check_prome_alive(prometheus_address): # dummy query is used to judge if prometheus is alive dummy_query = 'probe_success{}' return has_response(prometheus_address, dummy_query)def find_alive_prome(prometheus_addresses): if check_prome_alive(prometheus_addresses): return prometheus_addresses return None# ip:port -> ip_portdef decode_instance(instance): return instance.replace(':', '_')def check_metric(alert_name, prometheus_address, pql, is_value, pql_max): record = [] try: is_warning = "异样" response = request_prome(prometheus_address, pql) alert_name = alert_name.split('.') result = response.json()['data']['result'] # 判断是否出现异常 if len(result) == 0: is_warning = "失常" if pql_max == '': result = [{'metric': {}, 'value': [0, '0']}] else: response = request_prome(prometheus_address, pql_max) result = response.json()['data']['result'] for i in result: # 判断是否按节点显示 if 'instance' in i['metric']: instance = i['metric']['instance'] node = decode_instance(instance) else: node = '集群' # 判断是否有type if 'type' in i['metric']: type = i['metric']['type'] else: type = '无类型' value = i['value'][1] if value == 'NaN': value = 0 else: value = round(float(value), 3) message = "%s,%s,%s,%s,%s,%s,%s,%s" % ( datetime.datetime.now(), node, alert_name[1], alert_name[2], type, is_warning, is_value, value) print(message) record.append(message) except Exception as e: print(alert_name[2] + "----An error occurred check_metric:", e) return return recorddef csv_report(record): data = pd.DataFrame([line.split(',') for line in record], columns=['timestamp', 'ip_address', 'service', 'event_type', 'type', 'status', 'description', 'value']) grouped = data.groupby("service") writer = pd.ExcelWriter("inspection_report.xlsx", engine="xlsxwriter") for name, group in grouped: group.to_excel(writer, sheet_name=name, index=False) worksheet = writer.sheets[name] for i, col in enumerate(group.columns): column_len = max(group[col].astype(str).str.len().max(), len(col)) + 2 worksheet.set_column(i, i, column_len) writer.save()def run_tasks(role_metrics, prometheus_address): record = [] for alert in role_metrics: pql = role_metrics[alert]['pql'] is_value = role_metrics[alert]['note'] pql_max = role_metrics[alert]['pql_max'] message = check_metric(alert, prometheus_address, pql, is_value, pql_max) for data in message: record.append(data) csv_report(record)def run_script(prometheus_addresses): active_prometheus_address = find_alive_prome(prometheus_addresses) # check if all prometheus are down if not active_prometheus_address: sys.exit() tasks = get_tasks() run_tasks(tasks, active_prometheus_address)def get_user_input(): global days try: user_input = int(input("请输出须要巡检的天数: ")) days = user_input except ValueError: print("输出有效,请输出一个无效的数字。")if __name__ == "__main__": # 输出巡检天数 get_user_input() prometheus_ip = '10.3.65.136:9091' # prometheus_ip = None if prometheus_ip is None: cluster_name = get_cluster_name() cluster_info = display_cluster_info(cluster_name) id_role_dict = extract_id_role(cluster_info) print(id_role_dict) prometheus_ip = get_prometheus_ip(id_role_dict) print(prometheus_ip) run_script(prometheus_ip)
总结
一个欠缺的巡检脚本的编写是一个长期的工作。因为工夫无限,笔者只编写了基于 Prometheus 的一部分巡检项,有趣味的同学能够持续编写更多巡检项。目前巡检脚本都是基于 Prometheus 的数据来作判断,然而在实在的巡检当中,dba 还会查看一些 Prometheus 没有的数据,比方表的衰弱度、一段时间内的慢 SQL、热力求、日志信息等等,这些信息在前面一些工夫,可能会缓缓入到巡检脚本中。当初该脚本已在 Gitee 上开源,欢送大家应用:
https://gitee.com/mystery-cyf/prometheus--for-inspection/tree...