Подтвердить что ты не робот

Как справиться с производительностью создания подписанных URL-адресов для доступа к частному контенту через CloudFront?

Общим вариантом использования AWS S3 и CloudFront является предоставление личного контента. Обычным решением является использование подписанных URL CloudFront для доступа к приватным файлам, хранящимся с помощью S3.

Однако генерация этих URL-адресов сопряжена с затратами: вычисление RSA-подписи любого заданного URL-адреса с использованием закрытого ключа. Для Python (или boto, AWS Python SDK) для этой задачи используется библиотека rsa (https://pypi.python.org/pypi/rsa). На моем последнем MBB 2014 года требуется около 25 мс на вычисление с 2048-битным ключом.

Эта стоимость потенциально влияет на масштабируемость приложения, использующего этот подход для авторизации доступа к частному контенту через CloudFront. Представьте, что несколько клиентов запрашивают доступ к нескольким файлам часто при 25 ~ 30 мс /req.

Мне кажется, что очень мало можно улучшить для самого вычисления подписи, хотя упомянутая выше библиотека rsa была обновлена ​​почти 1,5 года назад. Интересно, есть ли другие методы или проекты, которые могут оптимизировать производительность этого процесса для достижения более высокой масштабируемости. Или нам просто нужно бросить больше оборудования и попытаться решить его грубостью?

Одна оптимизация может заключаться в том, чтобы конечная точка API принимала несколько подписок на каждый запрос и возвращала подписанные URL-адреса навалом, а не обрабатывала их отдельно в отдельных запросах, но общее время, необходимое для вычисления всех этих подписей, все еще существует.

4b9b3361

Ответ 1

Использовать подписанные файлы cookie

Когда я использую CloudFront со многими частными URL-адресами, я предпочитаю использовать Подписанные файлы cookie, когда все . Это не ускоряет создание подписанных файлов cookie, но уменьшает количество заявок на подписку на одного пользователя до истечения срока их действия.

Настройка генерации подписи RSA

Я могу представить, что у вас могут быть требования, которые делают подписанные файлы cookie недействительными. В этом случае я попытался ускорить подписание, сравнив модуль RSA, используемый с boto и cryptography. Два дополнительных альтернативных варианта: m2crypto и pycrypto но для этого примера я буду использовать криптографию.

Чтобы проверить эффективность подписи URL-адресов с разными модулями, я уменьшил метод _ sign_string, чтобы удалить любую логику, кроме подписания строки, затем созданной новый класс Distribution. Затем я взял секретный ключ и пример URL из boto tests для тестирования с помощью.

Результаты показывают, что криптография выполняется быстрее, но по-прежнему требуется около 1 мс при подписании запроса. Эти результаты были искажены выше при использовании IPython с использованием переменных с областью действия.

timeit -n10000 rsa_distribution.create_signed_url(url, message, expire_time)
10000 loops, best of 3: 6.01 ms per loop

timeit -n10000 cryptography_distribution.create_signed_url(url, message, expire_time)
10000 loops, best of 3: 644 µs per loop

Полный script:

from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

import rsa

from boto.cloudfront.distribution import Distribution

from textwrap import dedent

# The private key provided in the Boto tests
pk_key = dedent("""
    -----BEGIN RSA PRIVATE KEY-----
    MIICXQIBAAKBgQDA7ki9gI/lRygIoOjV1yymgx6FYFlzJ+z1ATMaLo57nL57AavW
    hb68HYY8EA0GJU9xQdMVaHBogF3eiCWYXSUZCWM/+M5+ZcdQraRRScucmn6g4EvY
    2K4W2pxbqH8vmUikPxir41EeBPLjMOzKvbzzQy9e/zzIQVREKSp/7y1mywIDAQAB
    AoGABc7mp7XYHynuPZxChjWNJZIq+A73gm0ASDv6At7F8Vi9r0xUlQe/v0AQS3yc
    N8QlyR4XMbzMLYk3yjxFDXo4ZKQtOGzLGteCU2srANiLv26/imXA8FVidZftTAtL
    viWQZBVPTeYIA69ATUYPEq0a5u5wjGyUOij9OWyuy01mbPkCQQDluYoNpPOekQ0Z
    WrPgJ5rxc8f6zG37ZVoDBiexqtVShIF5W3xYuWhW5kYb0hliYfkq15cS7t9m95h3
    1QJf/xI/AkEA1v9l/WN1a1N3rOK4VGoCokx7kR2SyTMSbZgF9IWJNOugR/WZw7HT
    njipO3c9dy1Ms9pUKwUF46d7049ck8HwdQJARgrSKuLWXMyBH+/l1Dx/I4tXuAJI
    rlPyo+VmiOc7b5NzHptkSHEPfR9s1OK0VqjknclqCJ3Ig86OMEtEFBzjZQJBAKYz
    470hcPkaGk7tKYAgP48FvxRsnzeooptURW5E+M+PQ2W9iDPPOX9739+Xi02hGEWF
    B0IGbQoTRFdE4VVcPK0CQQCeS84lODlC0Y2BZv2JxW3Osv/WkUQ4dslfAQl1T303
    7uwwr7XTroMv8dIFQIPreoPhRKmd/SbJzbiKfS/4QDhU
    -----END RSA PRIVATE KEY-----""")

# Initializing keys in a global context
cryptography_private_key = serialization.load_pem_private_key(
    pk_key,
    password=None,
    backend=default_backend())


# Instantiate a signer object using PKCS 1v 15, this is not recommended but required for Amazon
def sign_with_cryptography(message):
    signer = cryptography_private_key.signer(
        padding.PKCS1v15(),
        hashes.SHA1())

    signer.update(message)
    return signer.finalize()


# Initializing the key in a global context
rsa_private_key = rsa.PrivateKey.load_pkcs1(pk_key)


def sign_with_rsa(message):
    signature = rsa.sign(str(message), rsa_private_key, 'SHA-1')

    return signature


# All this information comes from the Boto tests.
url = "http://d604721fxaaqy9.cloudfront.net/horizon.jpg?large=yes&license=yes"
expected_url = "http://d604721fxaaqy9.cloudfront.net/horizon.jpg?large=yes&license=yes&Expires=1258237200&Signature=Nql641NHEUkUaXQHZINK1FZ~SYeUSoBJMxjdgqrzIdzV2gyEXPDNv0pYdWJkflDKJ3xIu7lbwRpSkG98NBlgPi4ZJpRRnVX4kXAJK6tdNx6FucDB7OVqzcxkxHsGFd8VCG1BkC-Afh9~lOCMIYHIaiOB6~5jt9w2EOwi6sIIqrg_&Key-Pair-Id=PK123456789754"
message = "PK123456789754"
expire_time = 1258237200


class CryptographyDistribution(Distribution):
    def _sign_string(
            self,
            message,
            private_key_file=None,
            private_key_string=None):
        return sign_with_cryptography(message)


class RSADistribution(Distribution):
    def _sign_string(
            self,
            message,
            private_key_file=None,
            private_key_string=None):
        return sign_with_rsa(message)


cryptography_distribution = CryptographyDistribution()
rsa_distribution = RSADistribution()

cryptography_url = cryptography_distribution.create_signed_url(
    url,
    message,
    expire_time)

rsa_url = rsa_distribution.create_signed_url(
    url,
    message,
    expire_time)

assert cryptography_url == rsa_url == expected_url, "URLs do not match"

Заключение

Хотя криптографический модуль работает лучше в этом тесте, я рекомендую попробовать найти способ использования подписанных файлов cookie, но я надеюсь, что эта информация будет полезна.

Ответ 2

Коротко

Считайте, можете ли вы (помимо использования python-cryptography, per @erik-e) использовать более короткую длину ключа (и, возможно, чаще менять ключи) учитывая данные вашего прецедента. Хотя я могу подписываться с 2048-битным ключом AWS, генерируемым в ~ 1550 мкс, он принимает только ~ 307 мкс при 1028 бит, ~ 184 мкс при 768 бит и ~ 113 мкс на 512 бит.

Объяснение

Изучив это немного, я собираюсь пойти в другом направлении и построить отличный ответ (уже большой), который дал @erik-e. Я должен упомянуть прежде, чем я получу в нем, что я не знаю, насколько приемлема эта идея; Я просто сообщаю о влиянии на производительность (см. Конец сообщения для вопроса, который я задал в SE для обеспечения безопасности).

Я собирал тайминги при подписании с cryptography, как предлагает @erik-e, и из-за еще большой разницы в производительности между ним и нашим существующим методом подписи для S3, я решил профилировать код, чтобы увидеть, похоже ли оно может быть что-то очевидное время пережевывания:

>>> cProfile.runctx('[sign_url_cloudfront2("...") for x in range(0,100)]', globals(), locals(), sort="time")
         9403 function calls in 0.218 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      200    0.161    0.001    0.161    0.001 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign}
      100    0.006    0.000    0.186    0.002 rsa.py:214(_finalize_pkey_ctx)
     1200    0.004    0.000    0.008    0.000 {isinstance}
      400    0.004    0.000    0.007    0.000 api.py:212(new)
      100    0.003    0.000    0.218    0.002 views.py:888(sign_url_cloudfront2)
      300    0.002    0.000    0.004    0.000 abc.py:128(__instancecheck__)
      100    0.002    0.000    0.008    0.000 hashes.py:53(finalize)
      200    0.002    0.000    0.005    0.000 gc_weakref.py:10(build)
      100    0.002    0.000    0.007    0.000 hashes.py:15(__init__)
      100    0.002    0.000    0.018    0.000 rsa.py:151(__init__)
      100    0.002    0.000    0.014    0.000 hashes.py:68(__init__)
      200    0.002    0.000    0.003    0.000 gc_weakref.py:14(remove)
      200    0.002    0.000    0.003    0.000 api.py:239(cast)
      100    0.002    0.000    0.190    0.002 rsa.py:207(finalize)
      200    0.001    0.000    0.007    0.000 api.py:325(gc)
      500    0.001    0.000    0.001    0.000 {getattr}
      400    0.001    0.000    0.001    0.000 {_cffi_backend.newp}
      400    0.001    0.000    0.001    0.000 api.py:150(_typeof)
      200    0.001    0.000    0.002    0.000 api.py:266(buffer)
      200    0.001    0.000    0.001    0.000 utils.py:18(<lambda>)
      300    0.001    0.000    0.001    0.000 _weakrefset.py:68(__contains__)
      200    0.001    0.000    0.001    0.000 {_cffi_backend.buffer}
      100    0.001    0.000    0.002    0.000 hashes.py:49(update)
      100    0.001    0.000    0.010    0.000 hashes.py:102(finalize)
      100    0.001    0.000    0.003    0.000 hashes.py:88(update)
      200    0.001    0.000    0.001    0.000 {method 'encode' of 'str' objects}
      100    0.001    0.000    0.019    0.000 rsa.py:528(signer)
      300    0.001    0.000    0.001    0.000 {len}
      100    0.001    0.000    0.001    0.000 base64.py:42(b64encode)
      100    0.001    0.000    0.008    0.000 backend.py:148(create_hash_ctx)
      200    0.001    0.000    0.001    0.000 {_cffi_backend.cast}
      200    0.001    0.000    0.001    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_get_digestbyname}
      100    0.001    0.000    0.001    0.000 {method 'format' of 'str' objects}
      100    0.001    0.000    0.003    0.000 rsa.py:204(update)
      200    0.000    0.000    0.000    0.000 {method 'pop' of 'dict' objects}
      100    0.000    0.000    0.000    0.000 {binascii.b2a_base64}
      200    0.000    0.000    0.000    0.000 {_cffi_backend.typeof}
      100    0.000    0.000    0.000    0.000 {time.time}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_DigestUpdate}
        1    0.000    0.000    0.218    0.218 <string>:1(<module>)
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_DigestInit_ex}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_new}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_free}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_DigestFinal_ex}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_create}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_rsa_padding}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_size}
      100    0.000    0.000    0.000    0.000 {method 'translate' of 'str' objects}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_cleanup}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_signature_md}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign_init}
      100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_destroy}
        1    0.000    0.000    0.000    0.000 {range}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Несмотря на то, что внутри signer существует небольшая сбережения, большая часть времени тратится внутри вызова finalize(), и почти все это время проводится внутри фактического вызова вызова в openssl. Хотя это было немного разочаровывающе, это был явный индикатор того, что я должен посмотреть на фактический процесс подписания сбережений.

Я просто использовал 2048-битный ключ CloudFront, сгенерированный для нас, поэтому я решил посмотреть, какое влияние на производительность будет иметь меньший ключ. Я снова запустил профиль, используя более короткий ключ:

>>> cProfile.runctx('[sign_url_cloudfront2("...") for x in range(0,100)]', globals(), locals(), sort="time")
        9203 function calls in 0.063 seconds

  Ordered by: internal time

  ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     100    0.008    0.000    0.008    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign}
     400    0.005    0.000    0.008    0.000 api.py:212(new)
     100    0.004    0.000    0.033    0.000 rsa.py:214(_finalize_pkey_ctx)
    1200    0.004    0.000    0.008    0.000 {isinstance}
     100    0.003    0.000    0.063    0.001 views.py:897(sign_url_cloudfront2)
     300    0.002    0.000    0.004    0.000 abc.py:128(__instancecheck__)
     100    0.002    0.000    0.008    0.000 hashes.py:53(finalize)
     200    0.002    0.000    0.005    0.000 gc_weakref.py:10(build)
     100    0.002    0.000    0.007    0.000 hashes.py:15(__init__)
     100    0.002    0.000    0.014    0.000 hashes.py:68(__init__)
     100    0.002    0.000    0.018    0.000 rsa.py:151(__init__)
     200    0.002    0.000    0.003    0.000 gc_weakref.py:14(remove)
     100    0.001    0.000    0.036    0.000 rsa.py:207(finalize)
     200    0.001    0.000    0.003    0.000 api.py:239(cast)
     200    0.001    0.000    0.006    0.000 api.py:325(gc)
     500    0.001    0.000    0.001    0.000 {getattr}
     200    0.001    0.000    0.002    0.000 api.py:266(buffer)
     400    0.001    0.000    0.001    0.000 {_cffi_backend.newp}
     400    0.001    0.000    0.001    0.000 api.py:150(_typeof)
     100    0.001    0.000    0.010    0.000 hashes.py:102(finalize)
     200    0.001    0.000    0.002    0.000 utils.py:18(<lambda>)
     300    0.001    0.000    0.001    0.000 _weakrefset.py:68(__contains__)
     100    0.001    0.000    0.002    0.000 hashes.py:88(update)
     100    0.001    0.000    0.001    0.000 hashes.py:49(update)
     200    0.001    0.000    0.001    0.000 {method 'encode' of 'str' objects}
     200    0.001    0.000    0.001    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_get_digestbyname}
     100    0.001    0.000    0.001    0.000 base64.py:42(b64encode)
     100    0.001    0.000    0.008    0.000 backend.py:148(create_hash_ctx)
     100    0.001    0.000    0.019    0.000 rsa.py:520(signer)
     200    0.001    0.000    0.001    0.000 {_cffi_backend.buffer}
     200    0.001    0.000    0.001    0.000 {method 'pop' of 'dict' objects}
     200    0.001    0.000    0.001    0.000 {_cffi_backend.cast}
     100    0.001    0.000    0.001    0.000 {method 'format' of 'str' objects}
     100    0.001    0.000    0.001    0.000 {time.time}
     100    0.001    0.000    0.003    0.000 rsa.py:204(update)
     200    0.000    0.000    0.000    0.000 {len}
     200    0.000    0.000    0.000    0.000 {_cffi_backend.typeof}
     100    0.000    0.000    0.000    0.000 {binascii.b2a_base64}
     100    0.000    0.000    0.000    0.000 {method 'translate' of 'str' objects}
       1    0.000    0.000    0.063    0.063 <string>:1(<module>)
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_DigestUpdate}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_new}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_DigestInit_ex}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_destroy}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_DigestFinal_ex}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_create}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign_init}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_size}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_cleanup}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_free}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_signature_md}
     100    0.000    0.000    0.000    0.000 {_Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_rsa_padding}
       1    0.000    0.000    0.000    0.000 {range}
       1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Как уже упоминалось в моем комментарии к erik-e, время выполнения, которое я видел для нашего полного метода подписи с использованием 2048-битного ключа с модулем cryptography, составляло ~ 1550 мкс. Повторение этого же теста с 512-битным ключом приводит к сокращению времени до ~ 113 мкс (каменный бросок от 30 мкс нашего метода подписи S3).

Этот результат кажется значимым, но он зависит от насколько приемлемым является использование более короткого ключа для вашей цели. Мне удалось найти комментарий с марта по отчету о выпуске Mozilla предлагая, что 512-битный ключ может быть разбит за 75 долларов за 8 часов на EC2.