Как я могу асинхронно вызывать множество URL-адресов из списка

У меня есть несколько сотен тысяч URL-адресов, которые мне нужно вызвать. Это обращения к серверу приложений, который их обработает и запишет код состояния в таблицу. Мне не нужно ждать ответа (успех/неудача), только чтобы сервер получил запрос. Я также хочу иметь возможность указать, сколько одновременных заданий может выполняться одновременно, поскольку я не определил, сколько одновременных запросов может обрабатывать tomcat.

Вот то, что у меня есть до сих пор, в основном взятое из чьей-то попытки сделать что-то подобное, только не с вызовами URL. Текстовый файл содержит каждый URL-адрес в отдельной строке. URL-адрес выглядит следующим образом:

http://webserver:8080/app/mwo/services/create?server=ServerName&e1user=admin&newMWONum=123456&sourceMWONum=0&tagNum=33-A-1B

И код:

$maxConcurrentJobs = 10
$content = Get-Content -Path "C:\Temp\urls.txt"

foreach ($url in $content) {
    $running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
    if ($running.Count -le $maxConcurrentJobs) {
        Start-Job {
             Invoke-WebRequest -UseBasicParsing -Uri $using:url
        }
    } else {
         $running | Wait-Job -Any
    }
    Get-Job | Receive-Job
}

Проблема, с которой я сталкиваюсь, заключается в том, что он дает 2 ошибки на «задание», и я не уверен, почему. Когда я выгружаю массив URL-адресов $content, он выглядит нормально, и когда я запускаю свой Invoke-WebRequest один за другим, они работают без ошибок.

126    Job126          BackgroundJob   Running       True            localhost            ...                
Invalid URI: The hostname could not be parsed.
    + CategoryInfo          : NotSpecified: (:) [Invoke-RestMethod], UriFormatException
    + FullyQualifiedErrorId : System.UriFormatException,Microsoft.PowerShell.Commands.InvokeRestMethodComman 
   d
    + PSComputerName        : localhost

Invalid URI: The hostname could not be parsed.
    + CategoryInfo          : NotSpecified: (:) [Invoke-RestMethod], UriFormatException
    + FullyQualifiedErrorId : System.UriFormatException,Microsoft.PowerShell.Commands.InvokeRestMethodComman 
   d
    + PSComputerName        : localhost

Любая помощь или альтернативные реализации будут оценены. Я открыт для того, чтобы не использовать powershell, но я ограничен настольными компьютерами Windows 7 или серверами Windows 2008 R2, и я, вероятно, буду запускать окончательный сценарий на самом сервере, используя localhost в URL-адресе, чтобы сократить сетевые задержки.


person trueimage    schedule 20.07.2015    source источник


Ответы (3)


Работая с заданиями, вы несете большие накладные расходы, потому что каждое новое задание порождает новый процесс.

Используйте пространства выполнения вместо этого!

$maxConcurrentJobs = 10
$content = Get-Content -Path "C:\Temp\urls.txt"

# Create a runspace pool where $maxConcurrentJobs is the 
# maximum number of runspaces allowed to run concurrently    
$Runspace = [runspacefactory]::CreateRunspacePool(1,$maxConcurrentJobs)

# Open the runspace pool (very important)
$Runspace.Open()

foreach ($url in $content) {
    # Create a new PowerShell instance and tell it to execute in our runspace pool
    $ps = [powershell]::Create()
    $ps.RunspacePool = $Runspace

    # Attach some code to it
    [void]$ps.AddCommand("Invoke-WebRequest").AddParameter("UseBasicParsing",$true).AddParameter("Uri",$url)

    # Begin execution asynchronously (returns immediately)
    [void]$ps.BeginInvoke()

    # Give feedback on how far we are
    Write-Host ("Initiated request for {0}" -f $url)
}

Как отмечено в связанном сообщении ServerFault, вы также можете использовать более общее решение, такое как Invoke-Parallel, которое в основном делает вышеописанное.

person Mathias R. Jessen    schedule 20.07.2015
comment
@mathias-r-jessen на самом деле есть ли способ, которым вы могли бы включить проверку ответа, которая выполняется асинхронно? в этой реализации ограничение одновременных заданий действительно ничего не делает, поскольку оно просто вызывает все URL-адреса. Мне нужно быть уверенным, что он получает как минимум код 200, если сервер приложений отказывает в соединении, он не будет обработан. - person trueimage; 20.07.2015
comment
@trueimage Я думал, вы сказали, что мне не нужно ждать ответа :) Это несколько усложняет получение ответа, когда вы вызываете их асинхронно, но да, это можно сделать. - person Mathias R. Jessen; 21.07.2015
comment
@mathias-r-jessen извините, мне не нужно ждать ответа (успех/неудача), нужно только, чтобы сервер получил запрос. - Мне нужно убедиться, что сервер действительно получил запрос, не вернул код ошибки. Это то, что делает мой исходный код (плохо). Я запустил образец из 1000 URL-адресов с вашим кодом, и на самом деле он обработал только 116, вероятно, потому, что он не может получать столько запросов за раз. Я рассмотрю ограничения на стороне сервера, но мне нужно убедиться, что это не просто ошибка. - person trueimage; 21.07.2015

Вы также можете использовать асинхронные методы веб-клиентов .net. Скажем, если вам просто нужно отправить запрос на получение ваших URL-адресов, Net.WebClient будет работать. Ниже приведен фиктивный пример с example.com:

$urllist = 1..97
$batchSize = 20

$results = [System.Collections.ArrayList]::new()

$i = 1
foreach($url in $urllist) {

  $w = [System.Net.Webclient]::new().DownloadStringTaskAsync("http://www.example.com?q=$i")
  $results.Add($w) | Out-Null

  if($i % $batchSize -eq 0 -or $i -eq $urllist.Count) {
      While($false -in $results.IsCompleted) {sleep -Milliseconds 300} # waiting for batch to complete
       Write-Host " ........   Batch completed   ......... $i" -ForegroundColor Green
       foreach($r in $results) { 
         New-Object PSObject -Property @{url = $r.AsyncState.AbsoluteURI; jobstatus =$r.Status; success = !$r.IsFaulted} 
         # if you need response text use $r.Result
       }
     $results.Clear()
   }

$i+=1

}
person Mike Twc    schedule 21.12.2018

Я согласен с верхним постом, чтобы использовать Runspaces. Однако предоставленный код не показывает, как получить данные из запроса. Вот модуль PowerShell, недавно опубликованный на моей странице GitHub:

https://github.com/phbits/AsyncHttps.

Он будет отправлять асинхронные HTTP-запросы в один домен через SSL/TLS (TCP-порт 443). Вот пример из README.md

Import-Module AsyncHttps
Invoke-AsyncHttps -DnsName www.contoso.com -UriPaths $('dir1','dir2','dir3')

Он возвращает System.Object[], содержащий результаты каждого запроса. Свойства результата следующие:

Uri       - Request Uri
Status    - Http Status Code or Exception Message
BeginTime - Job Start Time
EndTime   - Job End Time

После просмотра вашего примера вам, вероятно, потребуется внести следующие изменения:

  1. Разрешить использование альтернативного порта (webserver:8080). Проще всего было бы обновить URI в блоке скриптов. В качестве альтернативы добавьте еще один параметр в модуль и блок сценария только для порта.
  2. Убедитесь, что параметры запроса правильно отформатированы и не искажены процентным кодированием при использовании в HTTP-запросе. Подумайте о том, чтобы пропустить использование UriBuilder в блоке сценария, если известно, что ваш список путей Uri в порядке.
person phbits    schedule 07.05.2019