Как я могу создать огромное количество одновременных запросов HTTP HEAD в C #? - PullRequest
1 голос
/ 26 апреля 2019

У нас есть сервер, который выполняет манипуляции с изображениями на основе строки запроса и затем отображает результат. Результат также кэшируется на 90 дней. Из-за сложностей некоторые манипуляции могут занимать 6-7 секунд.

Рынок, на котором мы перечисляем некоторые из наших продуктов, недавно сократил их время ожидания при загрузке изображений до низкого значения, в результате чего большинство элементов в любом конкретном фиде впервые перестали работать из-за (их сообщения об ошибке) "Таймаута изображения". Когда мы повторно отправляем канал, таких проблем не возникает, поскольку наш сервер изображений теперь кэширует изображения.

Пожалуйста, , не предлагайте попросить рынок изменить время ожидания. Они смехотворно негибки и не сотрудничают. Также, пожалуйста, не предлагайте получить более мощный сервер изображений. На самом деле это огромная ферма, и моя команда ее не контролирует.

Это оставляет меня с одним вариантом. Мне нужно "заправить кеш" перед отправкой канала на рынок. Проблема заключается в том, что фид может содержать до 5000 элементов, каждый из которых содержит как минимум 2 изображения. Это означает, что 10000 изображений.

Я использую вызов HEAD, поскольку нам не нужно возвращать нам изображение. Я пытался использовать WebRequest и даже Socket в .Net Framework, вызываемом внутри асинхронного Task (используя Task.Run () `), но CLR будет раскручиваться только где-то около 20 задач одновременно. Поскольку, в среднем, каждое изображение занимает около 4 секунд (некоторые до 6-7 секунд, некоторые только 1 секунду), вы берете 10 000/20 = 500 * 4 секунд = 2000 секунд = 33 1/3 минуты, что не является приемлемое ожидание с нашей стороны, прежде чем мы отправим канал.

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

Мы используем AWS, поэтому я рассмотрел вопрос об использовании Lambdas, но это добавило бы дополнительную сложность и затраты, но массивная параллельная способность там звучит так, как будто это поможет.

Как я могу это исправить?

Тестовый сервер

public class HomeController : Controller {
    private Random random;
    public HomeController() {
        random = new Random(DateTime.UtcNow.Millisecond);
    }
    public ActionResult Index(string url) {
        var wait = random.Next(1, 70);
        Thread.Sleep(wait * 100);
        return Content(wait + " : " + url);
    }
}

Тестовый клиент

class Program {
    static void Main(string[] args) {
        var tasks = new List<Task>();
        for (var i = 0; i < 200; i++) {
            Console.WriteLine(i.ToString());
            var task = SendRequest("http://test.local.com/Home/Index?url=" + i);
            tasks.Add(task);
        }
        Task.WaitAll(tasks.ToArray());
    }
    private static async Task SendRequest(string url) {
        try {
            var myWebRequest = WebRequest.Create(url);
            myWebRequest.Method = "HEAD";
            var foo = await myWebRequest.GetResponseAsync();
            //var foo = myWebRequest.GetResponseAsync();
            //var foo = myWebRequest.GetResponse();
            foo.Dispose();
        }
        catch { }
    }
}

1 Ответ

1 голос
/ 08 мая 2019

Я ненавижу отвечать на свой вопрос, но я хочу поделиться тем, что я в конечном итоге сделал на тот случай, если кто-то столкнется с той же проблемой. По сути, я инкапсулировал код, который вызывает сервис изображений, в его собственный крошечный исполняемый файл, а затем использую Process.Start() для запуска исполняемого файла. Я определенно ожидал увидеть увеличение производительности, но я был удивлен, насколько сильно я увидел повышение. Увеличение было примерно в 20 раз, а загрузка ЦП на машине увеличивалась только до 20-40% в зависимости от того, сколько одновременных партий я запускал и насколько велики были партии.

В приведенном ниже коде учтите, что я удалил блоки try{}...catch{}, чтобы код был компактным.

Отдельный исполняемый файл (имя проекта ImageCachePrimer)

class Program {
    static void Main(string[] args) {
        var tasks = new List<Task>(args.Length);
        foreach (var url in args) {
            tasks.Add(Task.Run(async () => await SendRequest(url)));
        }
        Task.WaitAll(tasks.ToArray());
    }
    private static async Task SendRequest(string url) {
        var myWebRequest = WebRequest.Create(url);
        myWebRequest.Method = "HEAD";
        var foo = await myWebRequest.GetResponseAsync();
        foo.Dispose();
    }
}

Метод для вызова исполняемого файла.

private static Process CreateProcess(IEnumerable<string> urls)
{
    var args = urls.Aggregate("", (current, url) => current + url + " ");
    var start = new ProcessStartInfo();
    start.Arguments = args;
    start.FileName = "ImageCachePrimer.exe";
    start.WindowStyle = ProcessWindowStyle.Hidden;
    start.CreateNoWindow = false;
    start.UseShellExecute = true;
    return Process.Start(start);
}

Метод, который вызывает вышеуказанный метод

private static void PrimeImageCache(IReadOnlyCollection<string> urls) {
    var distinctUrls = urls.Distinct().ToList();
    const int concurrentBatches = 20;
    const int batchSize = 15;
    var processes = new List<Process>(concurrentBatches);
    foreach (var batch in distinctUrls.FormIntoBatches(batchSize)) {
        processes.Add(CreateProcess(batch));
        while (processes.Count >= concurrentBatches) {
            Thread.Sleep(500);
            for (var i = 0; i < processes.Count; i++) {
                var process = processes[i];
                if (process.HasExited) {
                    processes.Remove(process);
                }
            }
        }
    }
    while (processes.Count > 0) {
        Thread.Sleep(500);
        for (var i = 0; i < processes.Count; i++) {
            var process = processes[i];
            if (process.HasExited) {
                processes.Remove(process);
            }
        }
    }
}

Отдельный исполняемый файл и вызывающий его метод довольно просты. Я хотел бы объяснить некоторые нюансы в конечном методе. Сначала я пытался использовать foreach(var process in processes){process.WaitForExit();}, но это делало так, что каждый процесс в пакете должен был завершиться, прежде чем я смог запустить новый. Это также вызвало скачок процессора до 100% (я думаю, что внутри он делает почти пустой цикл, чтобы увидеть, завершен ли процесс). Итак, я «свернул свой собственный», как показано в первом цикле while. Во-вторых, мне пришлось добавить заключительный цикл while, чтобы убедиться, что у процессов, которые еще работали после того, как я поставил в очередь последний пакет в предыдущем foreach(), была возможность завершиться.

Надеюсь, это поможет кому-то еще.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...