From: Raza Shafiq (rshafiq) Date: Fri, 1 Sep 2023 20:02:20 +0000 (+0000) Subject: Pull request #3942: host_cache: segmented host cache X-Git-Tag: 3.1.70.0~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=852cc161efcf1e4f81c36ae6789cf6226e4f7793;p=thirdparty%2Fsnort3.git Pull request #3942: host_cache: segmented host cache Merge in SNORT/snort3 from ~RSHAFIQ/snort3:host_cache_locking to master Squashed commit of the following: commit e642b5dcfbc6a48be841676c6a9e77f2a8788dd3 Author: rshafiq Date: Thu Jul 27 08:43:35 2023 -0400 host_cache: added segmented host cache --- diff --git a/src/host_tracker/CMakeLists.txt b/src/host_tracker/CMakeLists.txt index 56dfa6806..8625723b5 100644 --- a/src/host_tracker/CMakeLists.txt +++ b/src/host_tracker/CMakeLists.txt @@ -2,6 +2,7 @@ set (HOST_TRACKER_INCLUDES cache_allocator.h cache_interface.h host_cache.h + host_cache_segmented.h host_tracker.h ) @@ -11,6 +12,7 @@ add_library( host_tracker OBJECT host_cache.cc host_cache_module.cc host_cache_module.h + host_cache_segmented.h host_tracker_module.cc host_tracker_module.h host_tracker.cc diff --git a/src/host_tracker/cache_allocator.cc b/src/host_tracker/cache_allocator.cc index f3e31fee5..2f046532b 100644 --- a/src/host_tracker/cache_allocator.cc +++ b/src/host_tracker/cache_allocator.cc @@ -26,11 +26,12 @@ #define CACHE_ALLOCATOR_CC #include "host_cache.h" +#include "host_cache_segmented.h" template HostCacheAllocIp::HostCacheAllocIp() { - lru = &host_cache; + lru = &default_host_cache; } #endif diff --git a/src/host_tracker/cache_allocator.h b/src/host_tracker/cache_allocator.h index cc8f5410c..ae392ad4d 100644 --- a/src/host_tracker/cache_allocator.h +++ b/src/host_tracker/cache_allocator.h @@ -38,6 +38,8 @@ public: T* allocate(std::size_t n); void deallocate(T* p, std::size_t n) noexcept; + void set_lru(CacheInterface* c) { lru = c; } + CacheInterface* get_lru() const { return lru; } protected: @@ -73,6 +75,7 @@ class HostCacheAllocIp : public CacheAlloc { public: + using Base = CacheAlloc; // This needs to be in every derived class: template struct rebind @@ -82,6 +85,21 @@ public: using CacheAlloc::lru; + void set_cache(CacheInterface* hci) { Base::set_lru(hci); } + CacheInterface* get_cache_ptr() { return Base::get_lru(); } + + template + HostCacheAllocIp(const HostCacheAllocIp& other) + { + this->lru = other.get_lru(); + } + + template + HostCacheAllocIp(HostCacheAllocIp&& other) noexcept + { + this->lru = other.get_lru(); + } + HostCacheAllocIp(); }; diff --git a/src/host_tracker/dev_notes.txt b/src/host_tracker/dev_notes.txt index 1a9a85361..5f659bf74 100644 --- a/src/host_tracker/dev_notes.txt +++ b/src/host_tracker/dev_notes.txt @@ -131,3 +131,53 @@ host_tracker.h and host_tracker.cc. Illustrative examples are test/cache_allocator_test.cc (standalone host cache / allocator example) and test/host_cache_allocator_ht_test.cc (host_cache / allocator with host tracker example). + +13/08/2023 + +To address the issue of contention due to mutex locks when Snort is configured +to run a large number (over 100) of threads with a single host_cache, +we introduced a new layer: "host_cache_segmented". This layer operates on +multiple cache segments, thus significantly reducing the locking contention +that was previously observed. + +The segmented host cache is not a replacement but rather an enhancement layer +above the existing host_cache. With this architecture, there can be more than +one host_cache, now referred to as a "segment". Each segment functions +as an LRU cache, just like the previous singular host_cache. Importantly, +there has been no change in the LRU cache design or its logic. + +Whenever a new key-data pair is added to a segment, its allocator needs updating. +This ensures that the peg counts and visibility metrics are accurate for +that specific segment. The find_else_create method of the segmented cache +takes care of this, ensuring that each key-data pair is correctly +associated with its segment. + +Each of these cache segments can operate independently, allowing for more +efficient parallel processing. This not only reduces the time threads spend +waiting for locks but also better utilizes multi-core systems by allowing +simultaneous read and write operations in different cache segments. + +The number of segments and the memcap are both configurable, providing flexibility +for tuning based on the specific requirements of the deployment environment +and the workload. Furthermore, this segmented approach scales well with the +increase in the number of threads, making it a robust solution for high-performance, +multi-threaded environments. + +In summary, the introduction of the "host_cache_segmented" layer represents +a significant step forward in the performance and scalability of Snort in +multi-threaded environments. This enhancement not only provides immediate benefits +in terms of improved throughput but also paves the way for further performance +optimizations in the future. + +-----------------+ + | Snort Threads | + +-----------------+ + | + v + +-------------------------------+ + | Host Cache Segmented Layer | + +-------------------------------+ + | + v + +-------------------------------------------------+ + | Cache Segment 1 | Cache Segment 2 | ... | + +-------------------------------------------------+ \ No newline at end of file diff --git a/src/host_tracker/host_cache.cc b/src/host_tracker/host_cache.cc index 61dcf5099..e4e7f4477 100644 --- a/src/host_tracker/host_cache.cc +++ b/src/host_tracker/host_cache.cc @@ -23,11 +23,9 @@ #endif #include "host_cache.h" +#include "host_cache_segmented.h" using namespace snort; -// Default host cache size in bytes. -// Must agree with default memcap in host_cache_module.cc. -#define LRU_CACHE_INITIAL_SIZE 16384 * 512 - -HostCacheIp host_cache(LRU_CACHE_INITIAL_SIZE); +HostCacheIp default_host_cache(LRU_CACHE_INITIAL_SIZE); +HostCacheSegmentedIp host_cache; diff --git a/src/host_tracker/host_cache.h b/src/host_tracker/host_cache.h index e68837c3a..06f81f977 100644 --- a/src/host_tracker/host_cache.h +++ b/src/host_tracker/host_cache.h @@ -36,6 +36,9 @@ #include "cache_allocator.h" #include "cache_interface.h" +// Default host cache size in bytes. +#define LRU_CACHE_INITIAL_SIZE 8388608 // 8 MB + // Used to create hash of key for indexing into cache. // // Note that both HashIp and IpEqualTo below ignore the IP family. @@ -276,6 +279,5 @@ public: } }; -extern SO_PUBLIC HostCacheIp host_cache; #endif diff --git a/src/host_tracker/host_cache_module.cc b/src/host_tracker/host_cache_module.cc index 1bcc2c216..f211fbd39 100644 --- a/src/host_tracker/host_cache_module.cc +++ b/src/host_tracker/host_cache_module.cc @@ -32,6 +32,7 @@ #include "log/messages.h" #include "managers/module_manager.h" #include "utils/util.h" +#include "host_cache_segmented.h" using namespace snort; using namespace std; @@ -62,6 +63,20 @@ static int host_cache_get_stats(lua_State* L) return 0; } +static int host_cache_get_segment_stats(lua_State* L) +{ + HostCacheModule* mod = (HostCacheModule*) ModuleManager::get_module(HOST_CACHE_NAME); + + if ( mod ) + { + int seg_idx = luaL_optint(L, 1, -1); + ControlConn* ctrlcon = ControlConn::query_from_lua(L); + string outstr = mod->get_host_cache_segment_stats(seg_idx); + ctrlcon->respond("%s", outstr.c_str()); + } + return 0; +} + static int host_cache_delete_host(lua_State* L) { HostCacheModule* mod = (HostCacheModule*) ModuleManager::get_module(HOST_CACHE_NAME); @@ -274,6 +289,12 @@ static const Parameter host_cache_stats_params[] = { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } }; +static const Parameter host_cache_segment_stats_params[] = +{ + { "segment", Parameter::PT_INT, nullptr, nullptr, "segment number for stats" }, + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + static const Parameter host_cache_delete_host_params[] = { { "host_ip", Parameter::PT_STRING, nullptr, nullptr, "ip address to delete" }, @@ -324,6 +345,7 @@ static const Command host_cache_cmds[] = { "delete_client", host_cache_delete_client, host_cache_delete_client_params, "delete client from host"}, { "get_stats", host_cache_get_stats, host_cache_stats_params, "get current host cache usage and pegs"}, + { "get_segment_stats", host_cache_get_segment_stats, host_cache_segment_stats_params, "get usage and pegs for cache segment(s)"}, { nullptr, nullptr, nullptr, nullptr } }; @@ -343,6 +365,9 @@ static const Parameter host_cache_params[] = { "memcap", Parameter::PT_INT, "512:maxSZ", "8388608", "maximum host cache size in bytes" }, + + { "segments", Parameter::PT_INT, "1:32", "4", + "number of host cache segments. It must be power of 2."}, { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } }; @@ -354,8 +379,25 @@ bool HostCacheModule::set(const char*, Value& v, SnortConfig*) dump_file = v.get_string(); } else if ( v.is("memcap") ) + { memcap = v.get_size(); + } + else if ( v.is("segments")) + { + segments = v.get_uint8(); + + if(segments > 32) + segments = 32; + if (segments == 0 || (segments & (segments - 1)) != 0) + { + uint8_t highestBitSet = 0; + while (segments >>= 1) + highestBitSet++; + segments = 1 << highestBitSet; + LogMessage("== WARNING: host_cache segments is not the power of 2. setting to %d\n", segments); + } + } return true; } @@ -366,8 +408,8 @@ bool HostCacheModule::end(const char* fqn, int, SnortConfig* sc) if ( Snort::is_reloading() ) sc->register_reload_handler(new HostCacheReloadTuner(memcap)); else - { - host_cache.set_max_size(memcap); + { + host_cache.setup(segments, memcap); ControlConn::log_command("host_cache.delete_host",false); } } @@ -438,20 +480,93 @@ void HostCacheModule::log_host_cache(const char* file_name, bool verbose) } +string HostCacheModule::get_host_cache_segment_stats(int seg_idx) +{ + + if(seg_idx >= host_cache.get_segments()) + return "Invalid segment index\nTry host_cache.get_segment_stats() to get all stats\n"; + + string str; + const PegInfo* pegs = host_cache.get_pegs(); + + if(seg_idx == -1) + { + const auto&& lru_data = host_cache.get_all_data(); + str = "Total host cache size: " + to_string(host_cache.mem_size()) + " bytes, " + + to_string(lru_data.size()) + " trackers, memcap: " + to_string(host_cache.get_max_size()) + + " bytes\n"; + + for(auto cache : host_cache.seg_list) + { + cache->lock(); + cache->stats.bytes_in_use = cache->current_size; + cache->stats.items_in_use = cache->list.size(); + cache->unlock(); + } + + PegCount* counts = (PegCount*) host_cache.get_counts(); + for ( int i = 0; pegs[i].type != CountType::END; i++ ) + { + if ( counts[i] ) + { + str += pegs[i].name; + str += ": " + to_string(counts[i]) + "\n" ; + } + } + } + + + str += "\n"; + str += "total cache segments: " + to_string(host_cache.seg_list.size()) + "\n"; + int idx = -1; + for( auto cache : host_cache.seg_list) + { + idx++; + if(seg_idx != -1 && seg_idx != idx) + continue; + + str += "Segment " + to_string(idx) + ":\n"; + const auto&& lru_data = cache->get_all_data(); + str += "Current host cache size: " + to_string(cache->mem_size()) + " bytes, " + + to_string(lru_data.size()) + " trackers, memcap: " + to_string(cache->get_max_size()) + + " bytes\n"; + + cache->lock(); + cache->stats.bytes_in_use = cache->current_size; + cache->stats.items_in_use = cache->list.size(); + cache->unlock(); + + PegCount* count = (PegCount*) cache->get_counts(); + for ( int i = 0; pegs[i].type != CountType::END; i++ ) + { + if ( count[i] ) + { + str += pegs[i].name; + str += ": " + to_string(count[i]) + "\n" ; + } + } + str += "\n"; + } + return str; +} + string HostCacheModule::get_host_cache_stats() { string str; const auto&& lru_data = host_cache.get_all_data(); str = "Current host cache size: " + to_string(host_cache.mem_size()) + " bytes, " - + to_string(lru_data.size()) + " trackers, memcap: " + to_string(host_cache.max_size) + + to_string(lru_data.size()) + " trackers, memcap: " + to_string(host_cache.get_max_size()) + " bytes\n"; - host_cache.lock(); - - host_cache.stats.bytes_in_use = host_cache.current_size; - host_cache.stats.items_in_use = host_cache.list.size(); - + for(auto cache : host_cache.seg_list) + { + cache->lock(); + cache->stats.bytes_in_use = cache->current_size; + cache->stats.items_in_use = cache->list.size(); + cache->unlock(); + } + PegCount* counts = (PegCount*) host_cache.get_counts(); const PegInfo* pegs = host_cache.get_pegs(); @@ -465,7 +580,6 @@ string HostCacheModule::get_host_cache_stats() } - host_cache.unlock(); return str; } @@ -478,14 +592,17 @@ PegCount* HostCacheModule::get_counts() const void HostCacheModule::sum_stats(bool dump_stats) { - host_cache.lock(); // These could be set in prep_counts but we set them here // to save an extra cache lock. - host_cache.stats.bytes_in_use = host_cache.current_size; - host_cache.stats.items_in_use = host_cache.list.size(); + for(auto cache : host_cache.seg_list) + { + cache->lock(); + cache->stats.bytes_in_use = cache->current_size; + cache->stats.items_in_use = cache->list.size(); + cache->unlock(); + } Module::sum_stats(dump_stats); - host_cache.unlock(); } void HostCacheModule::set_trace(const Trace* trace) const diff --git a/src/host_tracker/host_cache_module.h b/src/host_tracker/host_cache_module.h index 258f6b8f0..09fd1d1fc 100644 --- a/src/host_tracker/host_cache_module.h +++ b/src/host_tracker/host_cache_module.h @@ -31,6 +31,7 @@ #include "trace/trace_api.h" #include "host_cache.h" +#include "host_cache_segmented.h" #define HOST_CACHE_NAME "host_cache" #define HOST_CACHE_HELP "global LRU cache of host_tracker data about hosts" @@ -75,6 +76,7 @@ public: void log_host_cache(const char* file_name, bool verbose = false); std::string get_host_cache_stats(); + std::string get_host_cache_segment_stats(int seg_idx); void set_trace(const snort::Trace*) const override; const snort::TraceOption* get_trace_options() const override; @@ -82,6 +84,7 @@ public: private: std::string dump_file; size_t memcap = 0; + uint8_t segments = 1; }; extern THREAD_LOCAL const snort::Trace* host_cache_trace; diff --git a/src/host_tracker/host_cache_segmented.h b/src/host_tracker/host_cache_segmented.h new file mode 100644 index 000000000..e417435d5 --- /dev/null +++ b/src/host_tracker/host_cache_segmented.h @@ -0,0 +1,381 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2015-2023 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// This program 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 this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- +// host_cache_segmented.h author Raza Shafiq + +#ifndef HOST_CACHE_SEGMENTED_H +#define HOST_CACHE_SEGMENTED_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "host_cache.h" +#include "log/messages.h" + +#define DEFAULT_HOST_CACHE_SEGMENTS 4 + +extern SO_PUBLIC HostCacheIp default_host_cache; + +template +class HostCacheSegmented +{ +public: + HostCacheSegmented() : + segment_count(DEFAULT_HOST_CACHE_SEGMENTS), + memcap_per_segment(LRU_CACHE_INITIAL_SIZE) { } + HostCacheSegmented(uint8_t segment_count, size_t memcap_per_segment); + + void init(); + void term(); + void setup(uint8_t , size_t ); + + const PegInfo* get_pegs() { return lru_cache_shared_peg_names; } + size_t get_memcap_per_segment() { return memcap_per_segment.load(); } + size_t get_valid_id(uint8_t idx); + uint8_t get_segments() { return segment_count; } + size_t get_max_size(); + size_t get_mem_chunk(); + PegCount* get_counts(); + + void set_segments(uint8_t segments) { segment_count = segments; } + void print_config(); + bool set_max_size(size_t max_size); + bool reload_resize(size_t memcap_per_segment); + bool reload_prune(size_t new_size, unsigned max_prune); + void invalidate(); + + std::shared_ptr operator[](const Key& key); + + uint8_t get_segment_idx(Key val); + std::shared_ptr find(const Key& key); + std::shared_ptr find_else_create(const Key& key, bool* new_data); + std::vector>> get_all_data(); + bool find_else_insert(const Key& key, std::shared_ptr& value); + bool remove(const Key& key); + bool remove(const Key& key, typename LruCacheSharedMemcap + ::Data& data); + size_t mem_size(); + + std::vector seg_list; + HostCacheIp* default_cache = &default_host_cache; // Default cache used for host tracker + +private: + void update_counts(); + + uint8_t segment_count; + std::atomic memcap_per_segment; + struct LruCacheSharedStats counts; + bool init_done = false; +}; + + +template +HostCacheSegmented::HostCacheSegmented(uint8_t segment_count, size_t memcap_per_segment) : + segment_count(segment_count), + memcap_per_segment(memcap_per_segment) +{ + assert(segment_count > 0); + + for (size_t i = 0; i < this->segment_count; ++i) + { + auto cache = new HostCacheIp(this->memcap_per_segment); + seg_list.emplace_back((HostCacheIp*)cache); + } + init_done = true; +} + +template +void HostCacheSegmented::init() +{ + if(init_done or seg_list.size() >= segment_count) + return; + + assert(segment_count > 0); + + for (size_t i = 0; i < segment_count; ++i) + { + auto cache = new HostCacheIp(memcap_per_segment.load()); + seg_list.emplace_back((HostCacheIp*)cache); + } + init_done = true; +} + +template +void HostCacheSegmented::term() +{ + for (auto cache : seg_list) + { + if (cache) + delete cache; + } +} + +template +void HostCacheSegmented::setup(uint8_t segs, size_t memcap ) +{ + assert(segment_count > 0); + + segment_count = segs; + memcap_per_segment = memcap/segs; + set_max_size(memcap); +} + +template +size_t HostCacheSegmented::get_valid_id(uint8_t idx) +{ + if(idx < seg_list.size()) + return seg_list[idx]->get_valid_id(); + return 0; +} + +template +void HostCacheSegmented::print_config() +{ + if ( snort::SnortConfig::log_verbose() ) + { + snort::LogLabel("host_cache"); + snort::LogMessage(" memcap: %zu bytes\n", get_max_size()); + } +} + +template +std::shared_ptr HostCacheSegmented::operator[](const Key& key) +{ + return find_else_create(key, nullptr); +} + +/** + * Sets the maximum size for the entire cache, which is distributed equally + * among all the segments. + */ +template +bool HostCacheSegmented::set_max_size(size_t max_size) +{ + bool success = true; + memcap_per_segment = max_size/segment_count; + for (auto cache : seg_list) + { + if (!cache->set_max_size(memcap_per_segment)) + success = false; + } + return success; +} + +/** + * Resize the cache based on the provided memory capacity, distributing the + * memory equally among all the segments. If any segment fails to resize, + * the operation is considered unsuccessful. + */ +template +bool HostCacheSegmented::reload_resize(size_t memcap) +{ + bool success = true; + memcap_per_segment = memcap/segment_count; + for (auto cache : seg_list) + { + if (!cache->reload_resize(memcap_per_segment.load())) + success = false; + } + return success; +} + +// Computes the index of the segment where a given key-value pair belongs. +template +uint8_t HostCacheSegmented::get_segment_idx(Key val) +{ + const uint8_t* bytes = reinterpret_cast(&val); + uint8_t result = 0; + for (size_t i = 0; i < sizeof(Key); ++i) + result ^= bytes[i]; + //Assumes segment_count is a power of 2 always + //This is a fast way to do a modulo operation + return result & (segment_count - 1); +} + +//Retrieves all the data stored across all the segments of the cache. +template +std::vector>> HostCacheSegmented::get_all_data() +{ + std::vector>> all_data; + + for (auto cache : seg_list) + { + auto cache_data = cache->get_all_data(); + all_data.insert(all_data.end(), cache_data.begin(), cache_data.end()); + } + return all_data; +} + +template +std::shared_ptr HostCacheSegmented::find(const Key& key) +{ + uint8_t idx = get_segment_idx(key); + return seg_list[idx]->find(key); +} + +/** + * Updates the internal counts of the host cache. This method aggregates the + * counts from all segments and updates the overall counts for the cache. + */ +template +void HostCacheSegmented::update_counts() +{ + PegCount* pcs = (PegCount*)&counts; + const PegInfo* pegs = get_pegs(); + + for ( int i = 0; pegs[i].type != CountType::END; i++ ) + { + PegCount c = 0; + for(auto cache : seg_list) + { + c += cache->get_counts()[i]; + } + pcs[i] = c; + } +} + +template +std::shared_ptr HostCacheSegmented:: find_else_create(const Key& key, bool* new_data) +{ + // Determine the segment index where the key-value pair resides or should reside + uint8_t idx = get_segment_idx(key); + bool new_data_local = false; + + // Retrieve or create the entry for the key in the determined segment + auto ht = seg_list[idx]->find_else_create(key, &new_data_local); + if(new_data_local) + { + // If a new entry was created, update its cache interface and visibility + ht->update_cache_interface(idx); + ht->init_visibility(seg_list[idx]->get_valid_id()); + } + if(new_data) + *new_data = new_data_local; + return ht; +} + +template +bool HostCacheSegmented::find_else_insert(const Key& key, std::shared_ptr& value) +{ + uint8_t idx = get_segment_idx(key); + return seg_list[idx]->find_else_insert(key, value, false); +} + +template +PegCount* HostCacheSegmented::get_counts() +{ + if(init_done) + update_counts(); + return (PegCount*)&counts; +} + +template +void HostCacheSegmented::invalidate() +{ + for( auto cache: seg_list) + { + cache->invalidate(); + } +} + +template +bool HostCacheSegmented::reload_prune(size_t new_size, unsigned max_prune) +{ + bool success = true; + memcap_per_segment = new_size/segment_count; + for (auto cache : seg_list) + { + if (!cache->reload_prune(memcap_per_segment, max_prune)) + success = false; + } + return success; +} + +template +size_t HostCacheSegmented::mem_size() +{ + size_t mem_size = 0; + for (auto cache : seg_list) + { + if(cache) + mem_size += cache->mem_size(); + } + return mem_size; +} + +template +size_t HostCacheSegmented::get_max_size() +{ + size_t max_size = 0; + for (auto cache : seg_list) + { + max_size += cache->get_max_size(); + } + return max_size; +} + +template +size_t HostCacheSegmented::get_mem_chunk() +{ + //Assumes all segments have the same mem_chunk + return seg_list[0]->mem_chunk; +} + +template +bool HostCacheSegmented::remove(const Key& key) +{ + uint8_t idx = get_segment_idx(key); + return seg_list[idx]->remove(key); +} + +template +bool HostCacheSegmented::remove(const Key& key, typename LruCacheSharedMemcap::Data& data) +{ + uint8_t idx = get_segment_idx(key); + return seg_list[idx]->remove(key, data); +} + +/* +Warning!!!: update_allocator and update_set_allocator don't copy data to old container +but erase it for speed. Use with care!!! +*/ +template