Получение статистических значений из глубоко вложенного словаря с рекомендациями по ресурсам k8s в строке YAML

#python #pandas

Вопрос:

Поскольку Goldilocks поддерживает только машиночитаемый вывод через интерфейс командной строки, который ограничен пространством имен, я написал сценарий для очистки каждого модуля в каждом пространстве имен в каждом кластере с помощью запросов, kubernetes и beautifulsoup. Он в основном захватывает блоки кода YAML из DOM, которые выглядят следующим образом:

 resources:
  requests:
    cpu: 812m
    memory: 4506M
  limits:
    cpu: 1151m
    memory: 4999M
 

Затем я сбрасываю их в nested_dict, который выглядит следующим образом:

 "cluster": {
    "namespace": {
        "soa": "resources:
                 requests:
                   cpu: 812m
                   memory: 4506M
                 limits:
                   cpu: 1151m
                   memory: 4999M"
        }
    }
}
 

Оттуда я разбираю значения в другой вложенный индекс:

 soa_recs = nested_dict()
# First, flatten out a given cluster's items
for k, v in orig_dict[cluster_name].items_flat():
    for soa in k:
        # Example line for a given SOA:
        # ['resources:', '  requests:', '    cpu: 812m', '    memory: 4506M', '  limits:', '    cpu: 1151m', '    memory: 4999M']
        line = v.split("n")
        # Strip out whitespace and split values out
        request_cpu = line[2].strip().split(":")[1]
        request_mem = line[3].strip().split(":")[1]
        limit_cpu = line[5].strip().split(":")[1]
        limit_mem = line[6].strip().split(":")[1]
        # Fill new nested_dict with values
        soa_recs[k[0]][k[1]]["requests"]["cpu"] = request_cpu
        soa_recs[k[0]][k[1]]["requests"]["memory"]  = request_mem
        soa_recs[k[0]][k[1]]["limits"]["cpu"] = limit_cpu
        soa_recs[k[0]][k[1]]["limits"]["memory"] = limit_mem
 

Теперь мне нужно сгенерировать статистические значения для данного процессора SOA и запросов/ограничений памяти — возможно, достаточно минимального/максимального/медианного/среднего значения. Предпочтительно, чтобы у меня были эти значения для данного кластера, а также для n кластеров. Единицы измерения не гарантированы, но их можно разделить и сравнить, чтобы выполнить математику — например, память может быть представлена в M[B] или G[B].

Я мог бы просто пойти дальше с помощью nested_dict и, возможно, немного разобраться в диктанте и библиотеке статистики, но я чувствую, что Панды, вероятно, лучшее решение. Сброс nested_dict прямо в Pandas работает и дает мне это:

Фрейм данных Панд

Pandas достаточно умен, чтобы позволить мне разделить значения строк на основе их ключей, поэтому я предполагаю, что мог бы что-то сделать с помощью iloc и mean, но я не уверен, как это сделать. Потенциально добавляя изюминку, не все SOA развертываются во всех пространствах имен, и, конечно, это только один кластер, хотя наличие нескольких кадров данных, по одному для каждого кластера, не является нарушением соглашения.

Открыт для любых предложений, если есть более аккуратный способ сделать это; Я не женат на пандах.

Комментарии:

1. вы можете использовать df.stack().apply(pd.Series) для фильтрации значений NA, затем слева requests и limits в качестве имени столбцов. И индекс level_0-это необработанный индекс, индекс level_1 — имена необработанных столбцов.

2. Это работает, чтобы сделать его многоиндексным; все еще борется за создание значений. Я могу сделать что-то вроде следующего: df[«ограничения»][«прием api»].применить(лямбда x: x[«память»]).среднее значение() для одного значения, но было бы неплохо применить его ко всему сразу.

3. Есть некоторый прогресс с applymap(): например, df.applymap(лямбда x: x[«память»])[«ограничения»].max (). Просто нужно также выяснить, как получить ключ.

Ответ №1:

Я решил исправить это, разделив проблему на части, вместо того, чтобы пытаться выполнять операции со всем фреймом данных.

 # First, make a nested_dict so we don't have to deal with
# manually creating the levels.
mega_dict = nested_dict()

# Next, get clusters and namespaces, then loop through those.
for cluster_name, namespace_list in cluster_namespaces.items():
    print(f"Getting data from {cluster_name}")
    # For each namespace in a given cluster, parse the HTML.
    for namespace in namespace_list:
        html = get_html(cluster_name, domain, namespace)
        soup_dict = make_soup(html)
        # For a given SOA and its recommendations, populate our nested_dict.
        for soa, recs in soup_dict.items():
            mega_dict[cluster_name][namespace][soa] = recs


...

# For each cluster, loop through a flattened structure of the
# recommendation nested_dict. If it's a memory entry, convert the
# bytes (previously converted from whatever format it was in
# for consistent math) back into human readable format, then enter it.
# If it's a CPU entry, just append it. If it's anything else,
# something went wrong, so raise SystemExit and print an error.

cpu_values = []
memory_values = []
cpu_columns = ["namespace", "soa", "millicores"]
memory_columns = ["namespace", "soa", "size"]

for cluster in cluster_list:
    for k, v in recs[cluster][limit_or_rec].items_flat():
        if "memory" in k:
            memory_values.append((k[0], k[1], convert_bytes_to_human_readable(v, mib)))
        elif "cpu" in k:
            cpu_values.append((k[0], k[1], int(v)))
        else:
            print("Error finding recommendations - can you connect to the cluster?")
            raise SystemExit

...

# Create dataframes from our two lists.
cpu_df = pd.DataFrame(cpu_values, columns=cpu_columns, dtype=float)
memory_df = pd.DataFrame(
    memory_values, columns=memory_columns, dtype=float)

...

# Create another nested_dict, and create the entries from the dataframe
# with limits or requests as an arg, and appending either
# Mi/Gi for memory, or m (millicores) for CPU.
soa_dict = nested_dict()
soa_dict["resources"]["limits"]["cpu"] = f"{cpu_lim_df[('millicores'), lim_stat][soa]}m"
soa_dict["resources"]["limits"]["memory"] = f"{mem_lim_df[('size', lim_stat)][soa]}{unit}"
soa_dict["resources"]["requests"]["cpu"] = f"{cpu_req_df[('millicores', req_stat)][soa]}m"
soa_dict["resources"]["requests"]["memory"] = f"{mem_req_df[('size', req_stat)][soa]}{unit}"
 

Не уверен, что это все отражает; исходный код здесь.