Как быстро Вы можете идти? Оценка параллелизма между Python, JavaScript и Go | DedicateT.com

Регистрация
18.12.2017
Сообщения
804
Симпатии
384
Баллы
180
#1
Concurrency.png

Параллелизм является ключевой парадигмой для разработки масштабируемых, производительных и стабильных программных приложений. Такие языки, как JavaScript и Python, популяризировали асинхронные событийные шаблоны в пользу многопоточного программирования. Потоки оказались недостаточными в отношении производительности, потребления памяти и общего состояния, о чем свидетельствуют показатели производительности для серверов Nginx и Apache. В этом посте мы рассмотрим асинхронную производительность относительно Новичка в параллелизме веб-масштаба-Golang (Go).

Оценка трех языков
JavaScript и Python хорошо послужили мне для большинства моих проектов и являются абсолютным удовольствием для разработки, особенно при работе с веб-задачами. Я наслаждался этими языками в моей академической работе и публикациях, отраслевом инструменте и совсем недавно с личной проектной работой. Возможно, вы видели в последних сообщениях в блоге, которые я написал о инструменте перестановки домена - Domainsync, встроенном в JavaScript, и интерфейсе к REST API Burp, разработанному на Python.

Прежде чем приступить к другому инструменту сканирования безопасности, я хотел оценить, были ли мои языки go-To на самом деле лучшим выбором с точки зрения скорости и точности, или стоит ли кусать пулю и развиваться на совершенно новом языке. Чтобы оценить это, я создал один файл для каждого из трех языков [быстро., go, py, js], все используют лучшие методы параллелизма, поддерживаемые языками.

Процесс заключается в следующем:
  • Возьмите входной файл URL-адреса
  • Проанализируйте входной файл и создайте из него массив
  • Передать массив наиболее подходящей задаче параллелизма, поддерживаемой языком
Javascript
было решено использовать встроенную модель параллелизма JavaScript-Promises. Мы выбрали Promises.allметод, который по существу создает массив новых fetchобъектов и пытается разрешить их все сразу.

Код:
let urls = [...array of urls]
  Promise.all(urls.map(url =>        // resolve the array of fetch promises
    rp({uri: url})                   // exec request-promises for each url
    .then( res => console.log(res))  // print result
    .catch( err => console.log(err)) // catch errors
  ))
    .then(data => {                  // we are done
      console.log('done')
    })
Python
для Python библиотека asyncio python3 была выбрана из-за сходства поведения с async JavaScript.

Код:
## run
async def run():
    
    sem = asyncio.Semaphore(MAX_LIMIT)   # semaphor for maximum number of connections
    url = args.url                       # Retrieve the url as an argument from the command line
    clientsession = get_client_session() # Fetch all responses within one Client session

    await process(sem, url, clientsession)

## process
async def process(sem, url, clientsession):
    tasks = []

    r = 10000 #  number of async tasks to run
    async with clientsession as session: # use an async session across all connections
        for i in range(r):
            task = asyncio.ensure_future(bound_fetch(sem, url.format(i), session))
            tasks.append(task) # append the future to our tasks array

        responses = await asyncio.gather(*tasks) # await on all tasks to complete
        logger.debug('done')                     # we are finished

## bounded fetch
async def bound_fetch(sem, url, session):
    async with sem: # ensure we have the semaphore prior to awaiting our fetch
        return await fetch(url, session)

## fetch method
async def fetch(url, session)
    resp, source = None, None
    try:
        resp = await session.get(url, timeout=60, allow_redirects=True)
        source = await parse_response(url, resp)
        
    except:
        logger.debug('error occured')
    finally:
        if resp is not None:
            await resp.release()

    if resp:
        await resp.release()


## main
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(run())
loop.run_until_complete(future)
Golang
для Golang мы решили использовать встроенный примитив, а именно goroutines. Шаблон, используемый здесь, является производителем / потребителем, как показано в этой статье.

Код:
  // dispatcher
  func dispatcher(reqChan chan *http.Request, urls []string) {
    defer close(reqChan)                               // close the channel when ready
    for i := 0; i < reqs; i++ {                        // req's is the total number of requests to perform
      req, err := http.NewRequest("GET", urls[i], nil) // initialise our request object
      if err != nil {                                  // error check
        log.Println(err)
      }
      reqChan <- req                                   // send the request object to our request channel
    }
  }

  // worker pool
  func workerPool (reqChan chan *http.Request, respChan chan Response) {
    client := &http.Client{}               // client init & timeout settings defined here
    for i := 0; i < max; i++ {             // max is the maximum number of concurrent routines
      go worker(client, reqChan, respChan) // the go keyword instructs this method to run concurrently
    }
  }
        
  // worker
  func worker(client *http.Client, reqChan chan *http.Request, respChan chan Response) {
    for req := range reqChan {    // loop through requests on our requests channel
      resp, err := client.Do(req) // perform the HTTP request via *http.Client
      r := Response{resp, err}    // map the HTTP response to the Response struct
      respChan <- r               // send the result back on the response channel
    }
  }

  // consumer
  func consumer(respChan chan Response) (int64, int64) {
    var  conns  int64
    for conns < int64(reqs) {    // loop for total number of requests to perform
      select {
      case r, ok := <-respChan:  // receive the response from respChan
        if ok {
            processResponse(r)   // process the response

            if err := r.Body.Close(); err != nil {
              log.Println(r.err) // close and error log if failure occurs on close
            }
          conns++                // track no. of connections (this should be equivalent to # routines)
        }
      }
    }
    return conns                 // return no. of connections
  }

  // main
  urls:= []string{..slice of URLs}
  go dispatcher(reqChan, urls)      // dispatcher holding a pointer to our requests channel
  go workerPool(reqChan, respChan)  // worker pool holding pointers to request and response channels
  conns, size := consumer(respChan) // the consumer which processes our response channel
Возможно, вы заметили, что мы исключили последовательные запросы из нашей оценки - для аргументации они обычно занимают на порядок больше времени, чтобы завершить из-за времени ожидания для каждого предыдущего соединения, прежде чем перейти к следующему.

Методология
  1. Входной файл с 10,000 URL
  2. Попытка разрешить выборку для всех URL-адресов одновременно
  3. Распечатайте время, необходимое для завершения на каждом языке, используя время
Для справедливого и сбалансированного теста мы предполагаем следующее:
  • Выбранные URL-адреса кэшируются и поэтому не требуют попадания на исходные серверы (это приведет к искажению данных из-за несогласованной задержки с исходного сервера)
  • Тесты должны выполняться на одной и той же машине спецификации (2.9 GHz Intel Quad Core i7, память 16GB)
  • На тестовой машине не должно быть дополнительных процессов с интенсивной сетью
  • Несколько проходов для каждого языкового теста и усреднены по всем
  • Установлено ulimitзначение 10k+ для обеспечения достаточного количества открытых файловых дескрипторов для установления соединений сокетов
Результаты
Результаты показывают, что Go является явным и очевидным победителем здесь. Мы также обнаружили, что при увеличении количества одновременных запросов как Python, так и JavaScript выбрасывали исключения в основном из-за ошибок, связанных с тайм-аутом, в то время как Go просто продолжал "идти". Ниже статистика измеряется в секундах. (чем ниже, тем лучше)

1559971583180.png

Ограничения Python и JavaScript
В прошлом я обнаружил, что оба языка являются фантастическим выбором для веб-задач, инструментов и разработки API. Недостатки этих языков действительно проявляются только тогда, когда мы подталкиваем языки к их ограничениям ресурсов. И именно там, где нет поддержки первого класса для параллелизма, эти языки не дотягивают.

Для JavaScript у вас есть неблокирующая модель параллелизма цикла событий, которая рекламируется как один из наиболее эффективных методов при работе с HTTP как на стороне клиента, так и на стороне сервера. Этот подход называется решением для C10K проблема, от которой традиционно страдали серверы Apache. Nginx сделал подход цикла событий дико популярным и продемонстрировал, как масштабировать десятки тысяч одновременных HTTP-запросов, используя асинхронные шаблоны параллелизма (epoll, IOCompletionPorts), в отличие от многопоточности для сеансов запросов. Интенсивный поток памяти для подключения, используемый Apache, именно поэтому он подвержен атакам типа "отказ в обслуживании".

Python является более зрелым языком, чем JavaScript, и поддерживает как потоки пользовательского пространства, так и асинхронные процедуры. Основным недостатком использования Python в отношении параллелизма является то, что, как и Ruby, он страдает от глобальной блокировки интерпретатора (GIL) и поэтому не имеет истинной поддержки масштабируемой модели упрощенного параллелизма. Есть обходные пути для этой проблемы, но опять же, они являются дополнительными решениями проблемы параллелизма, которую ядро языка не решило.

Go, с другой стороны, был разработан Google для решения этих проблем. Язык программирования Go был детищем Роберта Грисемера, Роба Пайка и Кена Томпсона и был составлен для решения проблем модели параллелизма , выставленных языками, такими как Python, Java и C++. Таким образом, язык программирования Go имеет примитивы, такие как goroutines и каналы, испеченные с нуля, что дает пользователю возможность создавать подпрограммы и взаимодействовать потокобезопасным образом с другими параллельными задачами. Еще один ключевой вывод из использования goroutines-это тот факт, что они действительно дружественный к ЭКО по отношению к накладным расходам памяти, пока в то же время делающ его легким для потребителя синхронизировать между задачами. Вы можете думать о каналах как о легких нитях. Но в отличие от потоков в Java, которые требуют 1 Мб на поток только в накладных расходах, goroutines могут быть порождены стоимостью ~2 КБ в памяти без необходимости дополнительных библиотек.

Вывод
Ни для кого не секрет, что управляемый событиями подход JavaScript на протяжении многих лет приносил значительные преимущества по сравнению с традиционной потоковой обработкой для параллелизма, и именно поэтому nginx, как было показано, превосходит Apache в отношении проблемы C10K. Python также имеет множество преимуществ, особенно в отношении скорости, выразительности и множества ухоженных пакетов, что делает его языком для профессионалов, начиная от ученых-специалистов по данным, вплоть до профессионалов в области информационной безопасности. Однако, когда дело доходит до производительности и масштабируемости, эти языки могут быть не самым эффективным выбором по сравнению с языками параллелизма по дизайну, такими как Go. Наши эксперименты показывают, что Go является явным победителем при одновременной работе с десятками тысяч соединений, но требует немного больше работы с точки зрения структуры кода и многословия. На мой взгляд, эти компромиссы стоят того и в конечном итоге приносят дивиденды по мере усложнения кода. Дайте ему идти и дайте мне знать ваши мысли!
 

Пользователи, которые просматривали тему (Всего: 0)

Тема долгое время не просматривалась.