Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update socket config and create builder pattern #3929

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

gregcusack
Copy link

@gregcusack gregcusack commented Dec 5, 2024

Problem

We allocate large send and receive buffers for all sockets. However, not all sockets read and write equally from/to their buffers. In fact, 6 sockets are primarily Read and 2 are primarily Write. Meaning we are allocating memory that is never used.

Summary of Changes

preparation for reducing socket buffer sizes. This PR leaves socket buffer sizes as is.

  1. Update SocketConfig to use builder pattern
  2. Deprecate unused net-utils methods that use default SocketConfig

Services/Sockets
Read/Write

  1. Gossip
  2. RPC - TCP
  3. Ip_echo - TCP
  4. Tvu
  5. tvu_quic
  6. Repair
  7. Repair_quic
  8. Serve_repair
  9. Serve_repair_quic
  10. Ancestor_hashes_requests_quic
  11. Ancestor_hashes_requests

Primarily Read

  1. Tpu
  2. Tpu_forwards
  3. Tpu_vote
  4. Tpu_quic
  5. Tpu_forwards_quic
  6. Tpu_vote_quic

Primarily Write

  1. Retransmitter
  2. Broadcast

Follow Up PR:

  1. get rid of target gating
  2. Reduce size of unused side of socket
  3. Make better sizing decisions for the sockets that don't need a full 128MB buffer

gossip/src/cluster_info.rs Outdated Show resolved Hide resolved
@gregcusack gregcusack force-pushed the socket-config-builder branch 3 times, most recently from 707a6f2 to e8410b7 Compare December 6, 2024 01:13
@gregcusack gregcusack requested review from steviez and removed request for steviez December 6, 2024 02:38
}

#[cfg(not(any(windows, target_os = "ios")))]
impl Default for SocketConfig {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure this was my doing, but i don't really think a "default" socket config makes sense. default makes it too easy for people to instantiate a thing without thinking and in the case of these sockets, i think we want people to think about the config they're specifying

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the idea that we only expose a SocketConfig constructor that takes in: reuseport, send_buffer_size, and receive_buffer_size? and the user would have to explicitly set these?

I worry people would set these incorrectly. and having some default at least falls back on the original settings (using the equivalent buffer sizes previously set using sysctl)

Copy link
Author

@gregcusack gregcusack Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we don't have a default socket config, should we also not have DEFAULT_RECV_BUFFER_SIZE and DEFAULT_SEND_BUFFER_SIZE? just worried people are going to complain that they have to set these manually. And if they have to set them manually, they may just pick some large number (> 128MB) that ends up defeating the whole point of these PRs.

Also, the kernel doubles the buffer sizes passed in via setsockopt. So someone trying to set a buffer size of 128MB is unknowingly allocating 256MB to their sockets per side.

We could force users to set buffer sizes within some range. Minimum ~4MB and max 128MB? That way we limit misconfigurations to some extent

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of agree with Greg, if we're going to have default constants for fields, then I don't think it is too much of a stretch to have a default for the struct that contains those fields.

An alternative could be instead of Default implementation, we could have default_ro, default_wo and default_rw that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya we could have these default_ro, default_wo, default_rw. and then can have an option to set reuseport. we can also have the scaffold for setting buffer sizes directly like:

// set read buffer to 16MB and reuseport to true
let socket = SocketConfig::default_ro().recv_buffer_size(16 * 1024 * 1024).reuseport(true); 

For this PR we are not changing any of the actual buffer sizes. So, I propose for this PR, we provide default_wr that will set buffer sizes to the current 128MB. We leave out default_ro() and default_wo() on this PR since these defaults don't really make sense if we're not changing the buffer sizes on this PR

And then we at least have the functionality for the next PR where we can add default_ro() and default_wo() and set the buffer sizes

does that sound good?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me; thoughts @t-nelson ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more defaults are the wrong direction IMO. how about this concession..

#[derive(Default)]
pub struct SocketConfig {
    reuseport: bool,
    recv_buffer_size: Option<usize>,
    send_buffer_size: Option<usize>,
}

then when we build the socket, only even call setsockopt() when the field is Some(size)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case we just leave the socket buffer size set at whatever the default OS buffer size is? at the end of the day, someone is setting a default. it's either us or the OS. I'm ok with this idea tho

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we have little control over what the os does. odds are we shouldn't have custom "default" in the first place. the value from the tuning guide was just cribbed out of some blog post and tossed in iirc. no rigor to it. we should be tuning the socket buffers intelligently or not allocating sockets at all

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about this concession..

Works for me

net-utils/src/lib.rs Outdated Show resolved Hide resolved
net-utils/src/lib.rs Show resolved Hide resolved
net-utils/src/lib.rs Outdated Show resolved Hide resolved
net-utils/src/lib.rs Show resolved Hide resolved
@gregcusack gregcusack requested a review from t-nelson December 9, 2024 19:10
@gregcusack gregcusack force-pushed the socket-config-builder branch from e9c753e to cd98ecf Compare December 11, 2024 19:22
@gregcusack gregcusack requested review from steviez and t-nelson and removed request for steviez and t-nelson December 12, 2024 22:29
Copy link

@steviez steviez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we're getting close; just one question and a couple nits

let tpu_vote_quic_config = SocketConfig::default().reuseport(true);
let (tpu_vote_port, tpu_vote) =
Self::bind_with_config(bind_ip_addr, port_range, tpu_vote_udp_config.clone());
// using udp port for quic really because we need to reusport set to false, since Self::bind() defaults to false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// using udp port for quic really because we need to reusport set to false, since Self::bind() defaults to false
// using udp port for quic really because we need to set reusport to false, since Self::bind() defaults to false

nit: grammar

Also, can provide a little more detail on this piece; seems a little tricky

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly, no idea why i wrote this. doesn't really make sense. but happy to get some more details on this

let (_, broadcast) = Self::bind(bind_ip_addr, port_range);
let (_, ancestor_hashes_requests) = Self::bind(bind_ip_addr, port_range);
let (_, ancestor_hashes_requests_quic) = Self::bind(bind_ip_addr, port_range);
let write_only_socket_config = SocketConfig::default();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change to wo is a change in behavior right in this PR right ?

Copy link
Author

@gregcusack gregcusack Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya i forgot to change this back. i am going to change this. and set as just socket_config and also want to avoid using "read only" or "write only" since that's not technically correct with quic sockets.

multi_bind_in_range(bind_ip_addr, port_range, num_tvu_sockets.get())
.expect("tvu multi_bind");
let (tvu_quic_port, tvu_quic) = Self::bind(bind_ip_addr, port_range);
let tvu_config = SocketConfig::default().reuseport(true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: For consistency with higher in this function, it seems we might name this tvu_udp_config. There are a couple cases below too; I don't feel too strongly which way we go but I do prefer consistency

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so was thinking for this PR, since we are just adding this default, we just stick with:

let socket_config = SocketConfig::default();
// and
let socket_config_reuseport = SocketConfig::default().reuseport(true)

for the next PR when we change buffer sizes, maybe it makes sense to label these explicitly like tvu_config, tpu_forwards_config, etc. and here we would be setting buffer sizes.

@gregcusack gregcusack force-pushed the socket-config-builder branch from b84b41e to 296b463 Compare December 13, 2024 18:01
Comment on lines 389 to 395
#[derive(Clone, Debug, Default)]
pub struct SocketConfig {
pub reuseport: bool,
reuseport: bool,
#[cfg(not(any(windows, target_os = "ios")))]
recv_buffer_size: Option<usize>,
#[cfg(not(any(windows, target_os = "ios")))]
send_buffer_size: Option<usize>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add #[derive(Copy)] here and wouldn't need config.clone() elsewhere in the code.

Also, using #[cfg(..)] for struct fields probably isn't the best.
The fewer code differences we have across platforms is better, and here I don't think you need to change struct definition.

// allow here to supress unused warnings from windows/ios builds
#[allow(unused_mut, unused_variables)]
pub fn recv_buffer_size(mut self, size: usize) -> Self {
#[cfg(not(any(windows, target_os = "ios")))]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here (and perhaps other places in the code) there is no harm keeping the code the same across platforms.
so #[cfg(...)] here is probably unnecessary.

Copy link

@steviez steviez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Outside of the scope of this PR, but there is a little duplication in gossip/src/cluster_info.rs. For example, prior to this PR, this exact block shows up twice:

        let (_, repair) = Self::bind(bind_ip_addr, port_range);
        let (_, repair_quic) = Self::bind(bind_ip_addr, port_range);
        let (serve_repair_port, serve_repair) = Self::bind(bind_ip_addr, port_range);
        let (serve_repair_quic_port, serve_repair_quic) = Self::bind(bind_ip_addr, port_range);

Maybe as the SocketConfig tuning becomes more sophisticated in subsequent PR(s), it will make sense to refactor some stuff in a common helper (ie bind_repair()).

Wouldn't be the worst thing to get another approval from @behzadnouri or @t-nelson to make sure I didn't miss anything

Comment on lines -39 to -42
#[cfg(not(any(windows, target_os = "ios")))]
const DEFAULT_RECV_BUFFER_SIZE: usize = 64 * 1024 * 1024; // 64 MB - Doubled to 128MB by the kernel
#[cfg(not(any(windows, target_os = "ios")))]
const DEFAULT_SEND_BUFFER_SIZE: usize = 64 * 1024 * 1024; // 64 MB - Doubled to 128MB by the kernel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is line with intent from #3929 (comment), but just calling out this a potential change in behavior. I'm fine with it tho as:

  • People who aren't knowledgable on this probably copy/pasted tuning guide
  • People who are knowledgable probably set custom values
  • Subsequent PR(s) will set intelligent values, and those will hopefully come in quicker with the groundwork laid in this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants