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

feat: improved cross platform metric collection #2834

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

Conversation

NathanSavageKaimai
Copy link

This PR improves CPU and RAM metric collection across multiple environments. The CPU metrics are now fully cGroup aware report properly in containerised environments with cpu quota limits. The memory profile method on windows has been updated from using legacy WMIC to powershell. Finally two new utility methods have been added to @crawlee/utils, general.ts to determine if the scraper is containerised (instead of just running in docker) as well as if cGroup is enabled.

Adds

@crawlee/utils

general.ts

  • isContainerised() an extention of isDocker() that also checks for the presence of a KUBERNETES_SERVICE_HOST environment variable for k8 and a CRAWLEE_CONTAINERISED environment variable for manual control.
  • getCgroupsVersion() a method to determine the cGroup version in a cGroup controlled environment. It does this by checking for a file at /sys/fs/cgroup/memory/. If it is present, the cGroup verison is 1, else version 2.

cpu-info.ts

Collects cpu infomation in a similar manner to memory-info.ts

  • getCurrentCpuTicks() The existing solution. Used in AWS lambda, containerised environments without a cGroup cpu limit and on bare metal.
  • getCpuQuota() Gets the cpu quota in cGroup controlled environments.
  • getCpuPeriod() Gets the cpu quota period in cGroup controlled environments.
  • getContainerCpuUsage() Gets the containers cpu usage.
  • getSystemCpuUsage() Gets the systems cpu usage.
  • getCpuInfo() The main method for collecting cpu load metrics. Determines the enviroment and calls the other functions accordingly.

Removes

@apify/ps-tree

A legacy package that checked memory usage using WMIC.exe (depreciated) on windows or ps on *nix. Replaced by @crawlee\packages\utils\src\internals\psTree.ts which calculates the memory usage in a similar manner but using powershell and Get-CimInstance Win32_Process. Also adds type safety.

Fixes

Fixes: #2771

@NathanSavageKaimai
Copy link
Author

hey all, thanks for agreeing to take a look. I havent done much OSS before so im looking forward to hearing your thoughts :). Assuming it all looks good to you, when might it be incorperated into a crawlee release? At work we have a project that hinged on using crawlee in k8 so the autoscaling issues in containers is causing a fairly significant issue for us.

When you come to review it, id be happy to hop on a discord call and discuss it. :)

Thanks for everything you do!

@janbuchar
Copy link
Contributor

Hi @NathanSavageKaimai and thanks for your willingness to contribute! In the issue that this aims to close, you mentioned the possibility of using the ps-list package. If we decided that adding another dependency is fine, could the change be smaller? How much? Are there any other tradeoffs or possible disadvantages to using that library?

@NathanSavageKaimai
Copy link
Author

hi @janbuchar, ps-list would serve the same purpose as the new packages/utils/src/internals/psTree.ts file so it would be a 170 line reduction. Another module that might be useful that i have found since is systeminfomation. This module could likley replace, cpu-info.ts, memory-info.ts and psTree.ts leading to a ~540 line reduction. :)

Let me know if you would like me to explore these options.

@NathanSavageKaimai
Copy link
Author

with ps-list, you are relying on a third party binary which doesnt provide its source code as far as i can tell. It could be a potential supply chain risk.

@vladfrangu
Copy link
Member

I will +1 that worry, I'm not a fan of using a dependency that embeds a binary whose source code isn't directly open source / one we could build ourselves and embed

@janbuchar
Copy link
Contributor

Um, as far as I can tell, ps-list uses fastlist, which seems open enough to me - am I missing anything?

@NathanSavageKaimai
Copy link
Author

@janbuchar Ah yep, i hadnt found the cpp repo. still, being externally tracked, theres no automatic method to verify the authenticity of the binary beyond downloading from both sources and checking the hashes.

@vladfrangu
Copy link
Member

Um, as far as I can tell, ps-list uses fastlist, which seems open enough to me - am I missing anything?

The fact the binary is just embedded in instead of precompiled (like impit) or built at install time is a worry imo

@janbuchar
Copy link
Contributor

You both make a good point. I'd still consider exploring systeminformation - if we can avoid maintaining code for reading low level system details, it might be worth the increased install size.

@NathanSavageKaimai
Copy link
Author

You both make a good point. I'd still consider exploring systeminformation - if we can avoid maintaining code for reading low level system details, it might be worth the increased install size.

cool will do. :)

@NathanSavageKaimai
Copy link
Author

@janbuchar ive had a play around with systeminfomation and unfortunately it isnt as useful as i hoped it would be. It seems that the "docker" functions arent generating the metrics themselves but sending requests to the docker socket for the data. This unfortunately means that short of mounting the socket within the container, it can only work on the host. I have done a good search and as far as i can tell, there is no universal, cross platform library to collect cpu and ram metrics on "bare metal", cgroup 1 and 2. There might even be scope here for an entirely new package for apify but for now, my approach seems to be the best one going forward. :)

@janbuchar
Copy link
Contributor

Sounds reasonable, thanks! I will try to review the code in depth this week.

@NathanSavageKaimai
Copy link
Author

hey @janbuchar have you been able to have a look yet? No worries ethier way. if you like, we can sit down for a call later. Just shoot me a message on Discord - crafty5064. Im free until 2pm UTC or all day Saturday. :)

@NathanSavageKaimai
Copy link
Author

Hi all,

I hope you had a good weekend! Have you had a chance to review these changes? I was hoping they might be included in a release soon as these issues are blocking my company from deploying our product fully to k8. If you would like a chat, im free untill 1pm utc tomorrow. :)

@janbuchar
Copy link
Contributor

Hi, I'll look into it tomorrow. Sorry for the delay!

Copy link
Contributor

@janbuchar janbuchar left a comment

Choose a reason for hiding this comment

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

Good job on this one! I have a bunch of readability/code structure comments. More importantly though, the tests here are very narrowly scoped and use mocking heavily. Do you think you could add an E2E test that would verify that the system information detection works as expected? Feel free to suggest any other way to test this as a whole.

@@ -43,6 +43,35 @@ export async function isDocker(forceReset?: boolean): Promise<boolean> {
return isDockerPromiseCache;
}

/**
* Returns a `Promise` that resolves to true if the code is running in a containerised environment.
* Returns true if the CRAWLEE_CONTAINERISED environment variable is set.
Copy link
Contributor

Choose a reason for hiding this comment

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

Who is supposed to set the CRAWLEE_CONTAINERISED environment variable?

Copy link
Author

Choose a reason for hiding this comment

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

that is meant to be a manual way to run the containerised resource checks in case the other heuristics dont catch it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it. Could you add it to the documentation?

Copy link
Author

Choose a reason for hiding this comment

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

would you mind showing me where? im a little bit lost on that side of it, ta.

Im free for a call right now if you would like a chat. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

* @param includeRoot - Optional flag. When true, include the process with the given PID if found.
* Defaults to false.
*/
export async function psTree(pid: number | string, includeRoot: boolean = false): Promise<ProcessInfo[]> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very long and complex function. Possibly the main reason why it's hard to read is that it combines the implementation for UNIX and Windows in a single function. Could it be broken down into multiple smaller functions?

Copy link
Author

Choose a reason for hiding this comment

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

it was pretty much a copy paste from apify/pstree with the WMIC changes. I can reformat it. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Please do that, apify/pstree is super dated and I'm sure that if we don't refactor it now, we won't get back to it, ever.

Copy link
Author

Choose a reason for hiding this comment

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

cool will do :)

@NathanSavageKaimai
Copy link
Author

hi @janbuchar thanks for your insight! Most of the points were just me trying to follow conventions set out in the prexisting memory-info.ts file but i will definitely work on clarifying it. :)

@janbuchar
Copy link
Contributor

@NathanSavageKaimai please look into refactoring of psTree so that it's more readable.

Also, any thoughts regarding this?

More importantly though, the tests here are very narrowly scoped and use mocking heavily. Do you think you could add an E2E test that would verify that the system information detection works as expected? Feel free to suggest any other way to test this as a whole.

@NathanSavageKaimai
Copy link
Author

@NathanSavageKaimai please look into refactoring of psTree so that it's more readable.

Also, any thoughts regarding this?

More importantly though, the tests here are very narrowly scoped and use mocking heavily. Do you think you could add an E2E test that would verify that the system information detection works as expected? Feel free to suggest any other way to test this as a whole.

its a difficult one given that its so close to the metal, an e2e test would be dependant on the current state of the test runner unless i mocked the exec call and readline interface but at that point it may as well be a unit test. Also, personally I am only set up to run tests on windows or linux through wsl so i cant verify Macos compatability beyond "its a copy paste from a solution that persumably worked". What i will do is reimplement the unit tests in apify/pstree

@janbuchar
Copy link
Contributor

What i will do is reimplement the unit tests in apify/pstree

Cool, that will at least give us some certainty that ps-tree works.

I guess we could make a script that would show the current CPU and memory usage ratio and compare it with the old implementation. Then we could at least test-drive this on several machines with different OS and see if it behaves reasonably. What do you think?

@janbuchar
Copy link
Contributor

Hi @NathanSavageKaimai, we discussed this PR with @B4nan. Since this change impacts critical parts of Crawlee's functionality and testing it deeply enough requires time that we don't currently have (taking into account that you need this released soon), would you be open to making the new system info implementation opt-in using some kind of a feature flag?

If yes, we could probably release it and test it later, before we decide to make the new implementation the default.

@NathanSavageKaimai
Copy link
Author

Hi @janbuchar, sounds good, do you want me to make it an experiment? Also, any preference on what i call the flag?

Ta

@janbuchar
Copy link
Contributor

Yup, CrawlerExperiments sounds like the way to go. If you can't think of anything more descriptive, systemInfoNG or systemInfoV2 work just fine for me 🙂

@NathanSavageKaimai
Copy link
Author

The original metric collection has been restored and the new solution made toggleable with the systemInfoV2 experimental feature flag. I ended up needing to add the systemInfoV2 flag to the configuration class as localEventsManager isnt tied to a particular scraper. The only potential issue with this is that if someone was running multiple scrapers, some with the experiment and others without, it would use whichever setting was on the crawler instanciated last.

@NathanSavageKaimai
Copy link
Author

hi, just been doing some more tests and i found an issue, please dont merge yet.

ta

@NathanSavageKaimai
Copy link
Author

hi again, @janbuchar. I have fixed the scaling error. If you are all happy about it, perhaps we could merge it in soon? Thanks. :)

@janbuchar
Copy link
Contributor

Perfect! I think there are no major issues. We agreed with @B4nan that he'll quickly scan it and, if everything is fine, merge it. In the meantime, I'll try and test some risky changes we have waiting in the release branch so that we can make a stable release.

Thanks again for your contribution and for bearing with us 🙂

@NathanSavageKaimai
Copy link
Author

sounds good, just one more thing i have noticed, currently, the "TICKS_PER_SECOND" is hardcoded to 100, im going to introduce a quick check to read it from the kernel as i have found out that in some systems, rarely it can be different.

@NathanSavageKaimai
Copy link
Author

hi @B4nan, thanks for running the pull request toolkit, have you been able to have a look over the changes? No worries ethier way. :)

@NathanSavageKaimai
Copy link
Author

Hi @janbuchar have you been able to talk with @B4nan? my boss is asking for a timeline.

Ta

@B4nan
Copy link
Member

B4nan commented Feb 26, 2025

You will need to wait a bit, the PR is large, so it takes time. If you need to adopt this in your own project asap, I'd suggest you use something like https://github.com/ds300/patch-package instead of pushing us for merging it early.

@NathanSavageKaimai
Copy link
Author

Apologies, i didnt mean to offend. My understanding of the situation was that you were happy with janbuchar's review and you were going to simply scan it.

Thanks for all you are doing.

Copy link
Member

@B4nan B4nan left a comment

Choose a reason for hiding this comment

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

left a few comments, haven't seen the whole thing yet, but it's looking pretty good. i'd remove the experiments option in favor of the configuration one, having both of them feels weird, this is surely a system-wide option, so the config class is more appropriate for it

Comment on lines 381 to 385
/**
* Enables the use of the new resource management system.
* It should improve autoscaling in containerized environments by respecting cGroup resource limits.
*/
systemInfoV2?: boolean;
Copy link
Member

Choose a reason for hiding this comment

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

let's keep only the configuration option, it's weird to have two feature flags for one thing

Comment on lines 91 to 92
// TODO: check if this comment is still accurate
// this test hangs because we launch the browser, closing is apparently not enough?
Copy link
Member

Choose a reason for hiding this comment

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

i guess this is not relevant anymore, given the test doens't hang?

Comment on lines 120 to 121
// TODO: check if this comment is still accurate
// this test hangs because we launch the browser, closing is apparently not enough?
Copy link
Member

Choose a reason for hiding this comment

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

ditto

@@ -15,6 +15,8 @@ export const URL_NO_COMMAS_REGEX =
export const URL_WITH_COMMAS_REGEX =
/https?:\/\/(www\.)?([\p{L}0-9]|[\p{L}0-9][-\p{L}0-9@:%._+~#=]{0,254}[\p{L}0-9])\.[a-z]{2,63}(:\d{1,5})?(\/[-\p{L}0-9@:%_+,.~#?&/=()]*)?/giu;

export const FALSY_REGEX = /^false$/giu;
Copy link
Member

Choose a reason for hiding this comment

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

do we really need to export this?

Copy link
Author

Choose a reason for hiding this comment

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

not really, i just did that since the other regexes were exported

Comment on lines 97 to 98
log.exception(err as Error, 'Cpu snapshot failed.');
return {};
Copy link
Member

Choose a reason for hiding this comment

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

is this a good idea? downstream will expect values, and end up working with undefined instead

unless there is a good reason, i would remove the try/catch here

Copy link
Author

Choose a reason for hiding this comment

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

will do, i put that for parity with the existing createMemoryInfo function

* @returns a number between 0 and 1 for the cpu load
*/
export async function getCurrentCpuTicksV2(): Promise<number> {
if (await isContainerized()) {
Copy link
Member

Choose a reason for hiding this comment

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

check for the negation and return early so the huge nested block doesnt end up nested

Comment on lines 79 to 85
if (this.config.get('systemInfoV2')) {
const usedCpuRatio = await getCurrentCpuTicksV2();
return {
cpuCurrentUsage: usedCpuRatio * 100,
isCpuOverloaded: usedCpuRatio > options.maxUsedCpuRatio,
};
}
Copy link
Member

Choose a reason for hiding this comment

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

only this part needs to be in the try/catch if we want to keep it. but i would rather not have it here, if we want to have it somewhere, why not have it inside the getCurrentCpuTicksV2 method and return some meaningful defaults instead?

}

private async createMemoryInfo() {
try {
const memInfo = await this._getMemoryInfo();
let memInfo = { mainProcessBytes: -1, childProcessesBytes: -1 };
Copy link
Member

Choose a reason for hiding this comment

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

whats the point of the default value here? its always overridden

Copy link
Author

Choose a reason for hiding this comment

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

it was to properly scope the variable since memInfo is used in the same manner by v1 and v2, i will reformat

Comment on lines 60 to 63
if (process.env.CRAWLEE_CONTAINERIZED) {
isContainerizedResult = !FALSY_REGEX.test(process.env.CRAWLEE_CONTAINERIZED);
return isContainerizedResult;
}
Copy link
Member

Choose a reason for hiding this comment

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

ideally this would be handled via configuration class too, which normalizes env vars

Copy link
Author

Choose a reason for hiding this comment

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

unfortunately, we cannot access the Configuration class in utils, it would create a circular dependancy. I could refactor it into core?

@NathanSavageKaimai
Copy link
Author

hi @B4nan, working on those changes now. If we only want the configuration feature flag and not the experiment one, do you still want the experiment documentation page or shoudl i move it somewhere else?

Ta

@B4nan
Copy link
Member

B4nan commented Feb 27, 2025

yeah, let's keep the docs, that's fine

 - removed systemInfoV2 experiment flag in favour of config flag
 - added containerized / CRAWLEE_CONTAINERIZED config flag
 - isContainerized no longer checks the 'containerized' flag, instead 'containerized' is used in place of isContainerized in LocalEventManager
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.

Get rid of dependency on deprecated WMIC on Windows
4 participants