Stats for GekkoFS

This MR will provide the mechanism to store and check stats inside GekkoFS

There are two types of Stats: Number of operations (i.e., Create) and Operations with Size (i.e. write / read )

The stats are stored to allow the calculation of averages (and other stats) total, 1 min, 5 min and 10 minutes.

Other stats included: File-Chunk more accessed (write-read), using GKFS_CHUNK_STATS in the CMAKE. The structures can show the next info: accesses -- <file> // Chunk_id.

READ CHUNK MAP
1 -- /top/file_a // 4
2 -- /top/file_a // 3
3 -- /top/file_a // 1
/top/file_a // 2
5 -- /top/file_a // 0
WRITE CHUNK MAP
1 -- /top/file_a // 2
3 -- /top/file_a // 1
8 -- /top/file_a // 0

There is a thread for output the data each 10s to the console. The thread can be activated using a command line option, but the normal behaviour should be to allow an external RPC to gather the stats (i.e., a monitoring system).

Using --output-stats:

Stats IOPS_CREATE IOPS/s (avg, 1 min, 5 min, 10 min)                91.66 -         0 -         0 -         0 - 
Stats IOPS_WRITE IOPS/s (avg, 1 min, 5 min, 10 min)                 45.83 -         0 -         0 -         0 - 
Stats IOPS_READ IOPS/s (avg, 1 min, 5 min, 10 min)                  45.83 -         0 -         0 -         0 - 
Stats IOPS_STATS IOPS/s (avg, 1 min, 5 min, 10 min)                 320.8 -         0 -         0 -         0 - 
Stats IOPS_DIRENTS IOPS/s (avg, 1 min, 5 min, 10 min)               45.83 -         0 -         0 -         0 - 
Stats IOPS_REMOVE IOPS/s (avg, 1 min, 5 min, 10 min)                91.66 -         0 -         0 -         0 - 
Stats WRITE_SIZE MB/s (avg, 1 min, 5 min, 10 min)                  0.1353 -         0 -         0 -         0 - 
Stats READ_SIZE MB/s (avg, 1 min, 5 min, 10 min)                   0.1353 -         0 -         0 -         0 - 
Edited by Ramon Nou

Merge request reports

Loading
+1 −0
Changes for docs/sphinx/users/running.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@ Options:
                              RocksDB is default if not set. Parallax support is experimental.
                              Note, parallaxdb creates a file called rocksdbx with 8GB created in metadir.
  --parallaxsize TEXT         parallaxdb - metadata file size in GB (default 8GB), used only with new files
  --output-stats TEXT         Outputs the stats to the file each 10s.
  --version                   Print version and exit.
````

+250 −0
Changes for include/common/statistics/stats.hpp: 250 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*
  Copyright 2018-2022, Barcelona Supercomputing Center (BSC), Spain
  Copyright 2015-2022, Johannes Gutenberg Universitaet Mainz, Germany

  This software was partially supported by the
  EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu).

  This software was partially supported by the
  ADA-FS project under the SPPEXA project funded by the DFG.

  This file is part of GekkoFS.

  GekkoFS is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  GekkoFS is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with GekkoFS.  If not, see <https://www.gnu.org/licenses/>.

  SPDX-License-Identifier: GPL-3.0-or-later
*/

#ifndef GKFS_COMMON_STATS_HPP
#define GKFS_COMMON_STATS_HPP

#include <cstdint>
#include <unistd.h>
#include <cassert>
#include <map>
#include <set>
#include <vector>
#include <deque>
#include <chrono>
#include <initializer_list>
#include <thread>
#include <iostream>
#include <iomanip>
#include <fstream>
/**
 * Provides storage capabilities to provide stats about GekkoFS
 * The information is per server.
 * We do not provide accurate stats for 1-5-10 minute stats
 *
 */
namespace gkfs::utils {

/*
    Number of operations (Create, write/ read, remove, mkdir...)
    Size of database (metadata keys, should be not needed, any)
    Size of data (+write - delete)
    Server Bandwidth (write / read operations)

    mean, (lifetime of the server)
    1 minute mean
    5 minute mean
    10 minute mean

    To provide the stats that we need,
    we need to store the info and the timestamp to calculate it
    A vector should work, with a maximum of elements,
    The stats will only be calculated when requested
    a cached value will be send (with a deadline)
    */
class Stats {
public:
    enum class IOPS_OP {
        IOPS_CREATE,
        IOPS_WRITE,
        IOPS_READ,
        IOPS_STATS,
        IOPS_DIRENTS,
        IOPS_REMOVE,
    }; ///< enum storing IOPS Stats

    enum class SIZE_OP { WRITE_SIZE, READ_SIZE }; ///< enum storing Size Stats

private:
    constexpr static const std::initializer_list<Stats::IOPS_OP> all_IOPS_OP = {
            IOPS_OP::IOPS_CREATE,
            IOPS_OP::IOPS_WRITE,
            IOPS_OP::IOPS_READ,
            IOPS_OP::IOPS_STATS,
            IOPS_OP::IOPS_DIRENTS,
            IOPS_OP::IOPS_REMOVE}; ///< Enum IOPS iterator

    constexpr static const std::initializer_list<Stats::SIZE_OP> all_SIZE_OP = {
            SIZE_OP::WRITE_SIZE, SIZE_OP::READ_SIZE}; ///< Enum SIZE iterator

    const std::vector<std::string> IOPS_OP_S = {
            "IOPS_CREATE", "IOPS_WRITE",   "IOPS_READ",
            "IOPS_STATS",  "IOPS_DIRENTS", "IOPS_REMOVE"}; ///< Stats Labels
    const std::vector<std::string> SIZE_OP_S = {"WRITE_SIZE",
                                                "READ_SIZE"}; ///< Stats Labels

    std::chrono::time_point<std::chrono::steady_clock>
            start; ///< When we started the server

    const unsigned int MAX_STATS = 1000000; ///< How many stats will be stored


    std::map<IOPS_OP, unsigned long>
            IOPS; ///< Stores total value for global mean
    std::map<SIZE_OP, unsigned long>
            SIZE; ///< Stores total value for global mean

    std::map<IOPS_OP,
             std::deque<std::chrono::time_point<std::chrono::steady_clock>>>
            TIME_IOPS; ///< Stores timestamp when an operation comes removes if
                       ///< first operation if > 10 minutes Different means will
                       ///< be stored and cached 1 minuted


    std::map<enum SIZE_OP,
             std::deque<std::pair<
                     std::chrono::time_point<std::chrono::steady_clock>,
                     unsigned long long>>>
            TIME_SIZE; ///< For size operations we need to store the timestamp
                       ///< and the size


    std::thread t_output; ///< Thread that outputs stats info
    bool output_thread_;  ///< Enables or disables the output thread

    bool running =
            true; ///< Controls the destruction of the class/stops the thread
    /**
     * @brief Sends all the stats to the screen
     * Debug Function
     *
     * @param d is the time between output
     * @param file_output is the output file
     */
    void
    output(std::chrono::seconds d, std::string file_output);

    std::map<std::pair<std::string, unsigned long long>, unsigned int>
            CHUNK_READ; ///< Stores the number of times a chunk/file is read
    std::map<std::pair<std::string, unsigned long long>, unsigned int>
            CHUNK_WRITE; ///< Stores the number of times a chunk/file is write

    /**
     * @brief Called by output to generate CHUNK map
     *
     * @param output is the output stream
     */
    void
    output_map(std::ofstream& output);


    /**
     * @brief Dumps all the means from the stats
     * @param of Output stream
     */
    void
    dump(std::ofstream& of);

public:
    /**
     * @brief Starts the Stats module and initializes structures
     * @param output_thread creates an aditional thread that outputs the stats
     * @param filename file where to write the output
     */
    Stats(bool output_thread, std::string filename);

    /**
     * @brief Destroys the class, and any associated thread
     *
     */
    ~Stats();

    /**
     * @brief Adds a new read access to the chunk/path specified
     *
     * @param path
     * @param chunk
     */
    void
    add_read(std::string path, unsigned long long chunk);
    /**
     * @brief Adds a new write access to the chunk/path specified
     *
     * @param path
     * @param chunk
     */
    void
    add_write(std::string path, unsigned long long chunk);


    /**
     * Add a new value for a IOPS, that does not involve any size
     * No value needed as they are simple (1 create, 1 read...)
     * Size operations internally call this operation (read,write)
     *
     * @param IOPS_OP Which operation to add
     */

    void add_value_iops(enum IOPS_OP);

    /**
     * @brief Store a new stat point, with a size value.
     * If it involves a IO operations it will call the corresponding
     * operation
     *
     * @param SIZE_OP Which operation we refer
     * @param value to store (SIZE_OP)
     */
    void
    add_value_size(enum SIZE_OP, unsigned long long value);

    /**
     * @brief Get the total mean value of the asked stat
     * This can be provided inmediately without cost
     * @return mean value
     */
    double get_mean(enum IOPS_OP);


    /**
     * @brief Get the total mean value of the asked stat
     * This can be provided inmediately without cost
     * @return mean value
     */
    double get_mean(enum SIZE_OP);

    /**
     * @brief Get all the means (total, 1,5 and 10 minutes) for a SIZE_OP
     * Returns precalculated values if we just calculated them 1 minute ago
     *
     * @return std::vector< double > with 4 means
     */
    std::vector<double> get_four_means(enum SIZE_OP);

    /**
     * @brief Get all the means (total, 1,5 and 10 minutes) for a IOPS_OP
     * Returns precalculated values if we just calculated them 1 minute ago
     *
     * @return std::vector< double > with 4 means
     */
    std::vector<double> get_four_means(enum IOPS_OP);
};

} // namespace gkfs::utils

#endif // GKFS_COMMON_STATS_HPP
 No newline at end of file
+32 −0
Changes for include/daemon/classes/fs_data.hpp: 32 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -46,6 +46,11 @@ namespace data {
class ChunkStorage;
}

/* Forward declarations */
namespace utils {
class Stats;
}

namespace daemon {

class FsData {
@@ -85,6 +90,11 @@ private:
    bool link_cnt_state_;
    bool blocks_state_;

    // Statistics
    std::shared_ptr<gkfs::utils::Stats> stats_;
    bool output_stats_ = false;
    std::string stats_file_;

public:
    static FsData*
    getInstance() {
@@ -209,8 +219,30 @@ public:

    void
    parallax_size_md(unsigned int size_md);

    const std::shared_ptr<gkfs::utils::Stats>&
    stats() const;

    void
    stats(const std::shared_ptr<gkfs::utils::Stats>& stats);

    void
    close_stats();

    bool
    output_stats() const;

    void
    output_stats(bool output_stats);

    std::string
    stats_file() const;

    void
    stats_file(std::string stats_file);
};


} // namespace daemon
} // namespace gkfs

+281 −0
Changes for src/common/statistics/stats.cpp: 281 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*
  Copyright 2018-2022, Barcelona Supercomputing Center (BSC), Spain
  Copyright 2015-2022, Johannes Gutenberg Universitaet Mainz, Germany

  This software was partially supported by the
  EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu).

  This software was partially supported by the
  ADA-FS project under the SPPEXA project funded by the DFG.

  This file is part of GekkoFS.

  GekkoFS is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  GekkoFS is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with GekkoFS.  If not, see <https://www.gnu.org/licenses/>.

  SPDX-License-Identifier: GPL-3.0-or-later
*/


#include <common/statistics/stats.hpp>

using namespace std;

namespace gkfs::utils {

Stats::Stats(bool output_thread, std::string stats_file) {

    // Init clocks
    start = std::chrono::steady_clock::now();

    // To simplify the control we add an element into the different maps
    // Statistaclly will be negligible... and we get a faster flow

    for(auto e : all_IOPS_OP) {
        IOPS[e] = 0;
        TIME_IOPS[e].push_back(std::chrono::steady_clock::now());
    }

    for(auto e : all_SIZE_OP) {
        SIZE[e] = 0;
        TIME_SIZE[e].push_back(pair(std::chrono::steady_clock::now(), 0.0));
    }

    output_thread_ = output_thread;

    if(output_thread_) {
        t_output = std::thread([this, stats_file] {
            output(std::chrono::duration(10s), stats_file);
        });
    }
}

Stats::~Stats() {
    // We do not need a mutex for that
    if(output_thread_) {
        running = false;
        t_output.join();
    }
}

void
Stats::add_read(std::string path, unsigned long long chunk) {
    CHUNK_READ[pair(path, chunk)]++;
}

void
Stats::add_write(std::string path, unsigned long long chunk) {
    CHUNK_WRITE[pair(path, chunk)]++;
}


void
Stats::output_map(std::ofstream& output) {
    // Ordering
    map<unsigned int, std::set<pair<std::string, unsigned long long>>>
            ORDER_WRITE;

    map<unsigned int, std::set<pair<std::string, unsigned long long>>>
            ORDER_READ;

    for(auto i : CHUNK_READ) {
        ORDER_READ[i.second].insert(i.first);
    }

    for(auto i : CHUNK_WRITE) {
        ORDER_WRITE[i.second].insert(i.first);
    }

    auto CHUNK_MAP =
            [](std::string caption,
               map<unsigned int,
                   std::set<pair<std::string, unsigned long long>>>& ORDER,
               std::ofstream& output) {
                output << caption << std::endl;
                for(auto k : ORDER) {
                    output << k.first << " -- ";
                    for(auto v : k.second) {
                        output << v.first << " // " << v.second << endl;
                    }
                }
            };

    CHUNK_MAP("READ CHUNK MAP", ORDER_READ, output);
    CHUNK_MAP("WRITE CHUNK MAP", ORDER_WRITE, output);
}

void
Stats::add_value_iops(enum IOPS_OP iop) {
    IOPS[iop]++;
    auto now = std::chrono::steady_clock::now();


    if((now - TIME_IOPS[iop].front()) > std::chrono::duration(10s)) {
        TIME_IOPS[iop].pop_front();
    } else if(TIME_IOPS[iop].size() >= MAX_STATS)
        TIME_IOPS[iop].pop_front();

    TIME_IOPS[iop].push_back(std::chrono::steady_clock::now());
}

void
Stats::add_value_size(enum SIZE_OP iop, unsigned long long value) {
    auto now = std::chrono::steady_clock::now();
    SIZE[iop] += value;
    if((now - TIME_SIZE[iop].front().first) > std::chrono::duration(10s)) {
        TIME_SIZE[iop].pop_front();
    } else if(TIME_SIZE[iop].size() >= MAX_STATS)
        TIME_SIZE[iop].pop_front();

    TIME_SIZE[iop].push_back(pair(std::chrono::steady_clock::now(), value));

    if(iop == SIZE_OP::READ_SIZE)
        IOPS[IOPS_OP::IOPS_READ]++;
    else if(iop == SIZE_OP::WRITE_SIZE)
        IOPS[IOPS_OP::IOPS_WRITE]++;
}

/**
 * @brief Get the total mean value of the asked stat
 * This can be provided inmediately without cost
 * @return mean value
 */
double
Stats::get_mean(enum SIZE_OP sop) {
    auto now = std::chrono::steady_clock::now();
    auto duration =
            std::chrono::duration_cast<std::chrono::seconds>(now - start);
    double value = (double) SIZE[sop] / (double) duration.count();
    return value;
}

double
Stats::get_mean(enum IOPS_OP iop) {
    auto now = std::chrono::steady_clock::now();
    auto duration =
            std::chrono::duration_cast<std::chrono::seconds>(now - start);
    double value = (double) IOPS[iop] / (double) duration.count();
    return value;
}


/**
 * @brief Get all the means (total, 1,5 and 10 minutes) for a SIZE_OP
 * Returns precalculated values if we just calculated them 1 minute ago
 * // TODO: cache
 * @return std::vector< double > with 4 means
 */
std::vector<double>
Stats::get_four_means(enum SIZE_OP sop) {
    std::vector<double> results = {0, 0, 0, 0};
    auto now = std::chrono::steady_clock::now();
    for(auto e : TIME_SIZE[sop]) {
        auto duration =
                std::chrono::duration_cast<std::chrono::minutes>(now - e.first)
                        .count();
        if(duration > 10)
            break;

        results[3] += e.second;
        if(duration > 5)
            continue;
        results[2] += e.second;
        if(duration > 1)
            continue;
        results[1] += e.second;
    }
    // Mean in MB/s
    results[0] = get_mean(sop) / (1024.0 * 1024.0);
    results[3] /= 10 * 60 * (1024.0 * 1024.0);
    results[2] /= 5 * 60 * (1024.0 * 1024.0);
    results[1] /= 60 * (1024.0 * 1024.0);

    return results;
}


std::vector<double>
Stats::get_four_means(enum IOPS_OP iop) {
    std::vector<double> results = {0, 0, 0, 0};
    auto now = std::chrono::steady_clock::now();
    for(auto e : TIME_IOPS[iop]) {
        auto duration =
                std::chrono::duration_cast<std::chrono::minutes>(now - e)
                        .count();
        if(duration > 10)
            break;

        results[3]++;
        if(duration > 5)
            continue;
        results[2]++;
        if(duration > 1)
            continue;
        results[1]++;
    }

    results[0] = get_mean(iop);
    results[3] /= 10 * 60;
    results[2] /= 5 * 60;
    results[1] /= 60;

    return results;
}

void
Stats::dump(std::ofstream& of) {
    for(auto e : all_IOPS_OP) {
        auto tmp = get_four_means(e);

        of << "Stats " << IOPS_OP_S[static_cast<int>(e)]
           << " IOPS/s (avg, 1 min, 5 min, 10 min) \t\t";
        for(auto mean : tmp) {
            of << std::setprecision(4) << std::setw(9) << mean << " - ";
        }
        of << std::endl;
    }
    for(auto e : all_SIZE_OP) {
        auto tmp = get_four_means(e);

        of << "Stats " << SIZE_OP_S[static_cast<int>(e)]
           << " MB/s (avg, 1 min, 5 min, 10 min) \t\t";
        for(auto mean : tmp) {
            of << std::setprecision(4) << std::setw(9) << mean << " - ";
        }
        of << std::endl;
    }
    of << std::endl;
}
void
Stats::output(std::chrono::seconds d, std::string file_output) {
    int times = 0;
    std::ofstream of(file_output, std::ios_base::openmode::_S_trunc);

    while(running) {
        dump(of);
        std::chrono::seconds a = 0s;

        times++;
#ifdef GKFS_CHUNK_STATS
        if(times % 4 == 0)
            output_map(of);
#endif

        while(running and a < d) {
            a += 1s;
            std::this_thread::sleep_for(1s);
        }
    }
}

} // namespace gkfs::utils
+13 −0
Changes for src/common/CMakeLists.txt: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -39,10 +39,23 @@ target_sources(distributor
    ${CMAKE_CURRENT_LIST_DIR}/rpc/distributor.cpp
    )

add_library(statistics STATIC)
set_property(TARGET statistics PROPERTY POSITION_INDEPENDENT_CODE ON)
target_sources(statistics
    PUBLIC
    ${INCLUDE_DIR}/common/statistics/stats.hpp
    PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/statistics/stats.cpp
    )

if(GKFS_ENABLE_CODE_COVERAGE)
  target_code_coverage(distributor AUTO)
endif()

if(GKFS_ENABLE_CODE_COVERAGE)
  target_code_coverage(statistics AUTO)
endif()

# get spdlog
set(FETCHCONTENT_QUIET ON)

Loading
Loading