【问题标题】:Optimizing for Social Leaderboards优化社交排行榜
【发布时间】:2015-08-23 07:10:30
【问题描述】:

我正在使用 Google App Engine (python) 作为移动社交游戏的后端。该游戏使用 Twitter 集成,允许人们关注相关排行榜并与他们的朋友或追随者对战。

到目前为止,最昂贵的部分是后台(推送)任务,它通过 Twitter API 查询给定用户的朋友和关注者,然后将这些数据存储在我们的数据存储中。我正在尝试优化它以尽可能降低成本。

数据模型:

与这部分应用相关的主要模型有三个:

User
'''General user info, like scores and stats'''
# key id => randomly generated string that uniquely identifies a user
#   along the lines of user_kdsgj326
#   (I realize I probably should have just used the integer ID that GAE
#   creates, but its too late for that)

AuthAccount
'''Authentication mechanism.
     A user may have multiple auth accounts- one for each provider'''
# key id => concatenation of the auth provider and the auth provider's unique
#   ID for that user, ie, "tw:555555", where '555555' is their twitter ID
auth_id = ndb.StringProperty(indexed=True) # ie, '555555'
user = ndb.KeyProperty(kind=User, indexed=True)
extra_data = ndb.JsonProperty(indexed=False) # twitter picture url, name, etc.

RelativeUserScore
'''Denormalization for quickly generated relative leaderboards'''
# key id => same as their User id, ie, user_kdsgj326, so that we can quickly
#     retrieve the object for each user
follower_ids = ndb.StringProperty(indexed=True, repeated=True)
# misc properties for the user's score, name, etc. needed for leaderboard

我认为这个问题没有必要,但以防万一,here 是导致此设计的更详细的讨论。

任务

后台线程接收 Twitter 身份验证数据并通过 tweepy 从 Twitter API 请求一大块朋友 ID。 Twitter 默认发送多达 5000 个朋友 ID,如果可以避免的话,我宁愿不要随意限制更多(你每分钟只能向他们的 API 发出这么多请求)。

获得好友 ID 列表后,我可以轻松地将其转换为“tw:”AuthAccount 密钥 ID,然后使用 get_multi 检索 AuthAccounts。然后我删除所有不在我们系统中的 twitter 用户的 Null 帐户,并获取我们系统中的 twitter 朋友的所有用户 ID。这些 id 也是 RelativeUserScore 的键,所以我使用了一堆 transactional_tasklet 将这个用户的 ID 添加到 RelativeUserScore 的关注者列表中。

优化问题

  1. 发生的第一件事是调用 Twitter 的 API。鉴于任务中的其他所有内容都需要这样做,我假设我不会在使这个异步中获得任何收益,对吗? (GAE 已经足够聪明,可以在这个阻塞时使用服务器来处理其他任务?)
  2. 在确定是否有 twitter 朋友在玩我们的游戏时,我目前将所有 twitter 朋友 id 转换为 auth 帐户 ID,并通过 get_multi 检索。鉴于这些数据是稀疏的(大多数 Twitter 朋友很可能不会玩我们的游戏),我是否会更好地使用直接检索用户 ID 的投影查询?比如……

    twitter_friend_ids = twitter_api.friend_ids() # potentially 5000 values
    friend_system_ids = AuthAccount\
        .query(AuthAccount.auth_id.IN(twitter_friend_ids))\
        .fetch(projection=[AuthAccount.user_id])
    

    (我不记得或找不到在哪里,但我读这个更好,因为你不会浪费时间尝试读取不存在的模型对象

  3. 无论我最终使用 get_multi 还是投影查询,将请求分解为多个异步查询,而不是尝试一次获取/查询可能的 5000 个对象有什么好处?

【问题讨论】:

  • 您希望拥有多少个 Twitter ID(AuthAccount 实体),它们都可以放入内存吗?接下来,您希望多久运行一次此任务?
  • 每个使用 twitter 登录的用户都有一个对应的 AuthAccount(技术上更多,因为他们有一个系统 AuthAccount 用于他们的电子邮件/密码登录;但我可以添加一个过滤器来过滤掉这些)。我们需要一个可以扩展到非常大数量的系统。我知道大幅增长是一个长期的目标,但我们需要为大量用户设计,因为游戏会有一些促销活动。 “它们都适合记忆吗?”不知道你在这里到底是什么意思。它们不会都适合实例的内存。我怀疑任何用户都会有足够的追随者来玩,使他们不适合重复的 follower_ids
  • 至于这个任务的运行频率,我已经尝试过优化,并且会做更多。主要是在他们第一次使用他们的 Twitter 帐户登录时,它会通过他们所有的朋友和追随者。如果我们发现他们的朋友比以前多,它就会再次运行。我计划添加一些逻辑,以便它不会请求所有这些,直到它完成获得新朋友(twitter api 首先返回最新朋友)。然后我可能需要不定期检查(每月?每周?)来检查整个列表,以防他们删除了用户,或者添加了与他们删除的用户数量相同的用户,等等。

标签: python google-app-engine optimization twitter leaderboard


【解决方案1】:

我会这样组织任务:

  1. 对 Twitter 提要进行异步提取调用
  2. Use memcache 保存所有 AuthAccount->User 数据:
    • 从 memcache 请求数据,如果不存在,则调用 fetch_async()AuthAccount 以填充 memcache 和本地字典
  3. 通过 dict 运行每个 twitter ID

这里是一些示例代码:

future = twitter_api.friend_ids()    # make this asynchronous

auth_users = memcache.get('auth_users')
if auth_users is None:
    auth_accounts = AuthAccount.query()
                               .fetch(projection=[AuthAccount.auth_id,
                                                  AuthAccount.user_id])
    auth_users = dict([(a.auth_id, a.user_id) for a in auth_accounts])
    memcache.add('auth_users', auth_users, 60)

twitter_friend_ids = future.get_result()  # get async twitter results

friend_system_ids = []
for id in twitter_friend_ids:
    friend_id = auth_users.get("tw:%s" % id)
    if friend_id:
        friend_system_ids.append(friend_id)

这针对相对较少的用户和较高的请求率进行了优化。您上面的 cmets 表明用户数量较多且请求率较低,因此我只会对您的代码进行此更改:

twitter_friend_ids = twitter_api.friend_ids() # potentially 5000 values
auth_account_keys = [ndb.Key("AuthAccount", "tw:%s" % id) for id in twitter_friend_ids]
friend_system_ids = filter(None, ndb.get_multi(auth_account_keys))

当使用get_multi() 和键时,这将使用ndb 的内置memcache 来保存数据。

【讨论】:

  • 正如您在第一种方法中所指出的,它针对不同的目标进行了优化。至于第二种方法,我目前正在这样做:tw_auth_accounts = ndb.get_multi([ndb.Key(AuthAccount, "tw:%s" % id) for id in tw_ids])。与仅使用 get_multi(...) 相比, map(None, get_mult(...)) 有什么好处?
  • map() 调用将过滤掉 None 值。
  • 嗯,我错过了什么吗? x = map(None, [None,1,2,3]) 返回 [None, 1, 2, 3]。无论如何,我只是使用列表推导来过滤掉它们。
  • 我的错,应该是filter()。列表推导同样有效。
  • 好的,所以我只是做了一堆负载测试来比较这两个选项。在 get_multi 上使用投影查询似乎没有任何好处(RPC 或数据存储读取/写入)。由于内存缓存(如您所建议的), get_multi 通常更便宜。此外,查询方法通常也较慢(b/c 执行 IN 查询似乎本质上是对每个相等性执行一堆单独的查询,除了它按顺序完成而不是并行完成)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-08-21
  • 2018-07-18
  • 2016-08-14
  • 2018-01-06
  • 2016-09-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多