目前用开发版,自己一个人做测试时,有时就会提示 LeanCloudError: LeanCloudError: [429] Too many requests. 在控制台看到的数据如下图所示:

被请求的主要是一个云函数API。当发生这个错误时,unity会卡死需要手动结束进程。

问题:
1. 现在只单人测试一个简单的小功能就达到了最高7个线程,而商用版上线30个线程,是不是因为我客户端设计的不好导致发送的请求频次太高?
2. 当发生短时请求量大时,超出的请求不是会放入队列延后处理吗?报错是不是说明这个请求就根本没被接受没被处理?这样的话在unity客户端应该如何处理避免unity死机?
3. 如果是因为我设计的问题,那么一个解决思路是,把多个该云函数的请求合并为一个请求从unity客户端发送到leancloud服务器;在服务器端写一个新的云函数接口,但在其中还是会短时多次调用旧的接口。请问这样是否可以解决该问题?(也就是想问在服务器端进行的云函数接口调用、数据库查询是否会增加这个工作线程数?是否会记入每天30000请求的总量限制?)
4. 是否有其他建议?

非常感谢!

我们有很多量非常大的应用也是靠 30 个工作线程支撑的,一般来说这方面很少有不够用的问题。工作线程的说明可以看 FAQ 里的说明20。单人测试的时候出现 429 错误一般都是开发上的问题,比如测试时写出了死循环,或者因为重复刷新导致产生了非常密集的请求。

不知道你是怎么使用我们服务的,如果是集成了 SDK,那么判断一下异常的错误码是否为 429 再做相应处理就行。如果是直接请求 REST API,那么就判断一下返回的状态码 / 错误码就成。

感谢回复,我是用Unity里集成了SDK,错误信息是在云引擎网页上看到的。错误码是429没错。在被调用的那个云函数中,我写了一个print,出错时这个print在云引擎网页上看一瞬间被调用了大概10次。

我在客户端又测试了一下,发生错误时,只分三个瞬间调用了总计12次云函数,分别为6、3、3。这次出错时,在云引擎客户端有以下error,无429错误,unity死机。

ERROR web1 14:19:05 Traceback (most recent call last):
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/gevent/pywsgi.py", line 884, in handle_one_response
ERROR web1 14:19:05     self.run_application()
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/geventwebsocket/handler.py", line 88, in run_application
ERROR web1 14:19:05     return super(WebSocketHandler, self).run_application()
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/gevent/pywsgi.py", line 870, in run_application
ERROR web1 14:19:05     self.result = self.application(self.environ, self.start_response)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/__init__.py", line 52, in __call__
ERROR web1 14:19:05     return self.cloud_app(environ, start_response)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/werkzeug/local.py", line 228, in application
ERROR web1 14:19:05     return ClosingIterator(app(environ, start_response), self.cleanup)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/cors.py", line 51, in __call__
ERROR web1 14:19:05     return self.app(environ, cors_start_response)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/authorization.py", line 43, in __call__
ERROR web1 14:19:05     return self.app(environ, start_response)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/leanengine.py", line 73, in __call__
ERROR web1 14:19:05     self.process_session(environ)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/leanengine.py", line 84, in process_session
ERROR web1 14:19:05     user = leancloud.User.become(session_token)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/user.py", line 59, in become
ERROR web1 14:19:05     response = client.get('/users/me', params={'session_token': session_token})
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/client.py", line 90, in new_func
ERROR web1 14:19:05     return func(headers=headers, *args, **kwargs)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/client.py", line 171, in new_func
ERROR web1 14:19:05     response = func(*args, **kwargs)
ERROR web1 14:19:05   File "/usr/local/lib/python2.7/dist-packages/leancloud/client.py", line 163, in new_func
ERROR web1 14:19:05     raise leancloud.LeanCloudError(content.get('code', 1), content.get('error', 'Unknown Error'))
ERROR web1 14:19:05 LeanCloudError: LeanCloudError: [429] Too many requests.
ERROR web1 14:19:05 {'CONTENT_LENGTH': '33',
ERROR web1 14:19:05  'CONTENT_TYPE': 'application/json',
ERROR web1 14:19:05  'GATEWAY_INTERFACE': 'CGI/1.1',
ERROR web1 14:19:05  'HTTP_ACCEPT': '*/*',
ERROR web1 14:19:05  'HTTP_CONNECTION': 'close',
ERROR web1 14:19:05  'HTTP_HOST': 'api.leancloud.cn',
ERROR web1 14:19:05  'HTTP_USER_AGENT': 'UnityPlayer/5.5.0f3 (http://unity3d.com)',
ERROR web1 14:19:05  'HTTP_X_FORWARDED_FOR': '::ffff:10.10.44.101',
ERROR web1 14:19:05  'HTTP_X_FORWARDED_PORT': '80',
ERROR web1 14:19:05  'HTTP_X_FORWARDED_PROTO': 'https',
ERROR web1 14:19:05  'HTTP_X_FORWARDED_PROTOCOL': 'http',
ERROR web1 14:19:05  'HTTP_X_LC_CLIENT_VERSION': 'net-portable-2.0.0.0',
ERROR web1 14:19:05  'HTTP_X_LC_ID': 'GSIRUlB5taiAdvjI1jynXHu7-gzGzoHsz',
ERROR web1 14:19:05  'HTTP_X_LC_INSTALLATION_ID': 'ce91dfe8-3c0d-455f-bae1-354fab2fe3ad',
ERROR web1 14:19:05  'HTTP_X_LC_KEY': 'zPc1Cs5CKAr2i0QGPxtHhOma',
ERROR web1 14:19:05  'HTTP_X_LC_SESSION': '28l6z4jr28u2b8lo84f74nide',
ERROR web1 14:19:05  'HTTP_X_REAL_IP': '125.33.93.118',
ERROR web1 14:19:05  'HTTP_X_UNITY_VERSION': '5.5.0f3',
ERROR web1 14:19:05  'PATH_INFO': '/1.1/functions/species',
ERROR web1 14:19:05  'QUERY_STRING': '',
ERROR web1 14:19:05  'REMOTE_ADDR': '10.10.12.239',
ERROR web1 14:19:05  'REMOTE_PORT': '43700',
ERROR web1 14:19:05  'REQUEST_METHOD': 'POST',
ERROR web1 14:19:05  'SCRIPT_NAME': '',
ERROR web1 14:19:05  'SERVER_NAME': 'c4864a1e2504',
ERROR web1 14:19:05  'SERVER_PORT': '3000',
ERROR web1 14:19:05  'SERVER_PROTOCOL': 'HTTP/1.1',
ERROR web1 14:19:05  'SERVER_SOFTWARE': 'gevent/1.1 Python/2.7',
ERROR web1 14:19:05  '_app_params': {'id': u'GSIRUlB5taiAdvjI1jynXHu7-gzGzoHsz',
ERROR web1 14:19:05                  'key': u'zPc1Cs5CKAr2i0QGPxtHhOma',
ERROR web1 14:19:05                  'master_key': None,
ERROR web1 14:19:05                  'session_token': u'28l6z4jr28u2b8lo84f74nide'},
ERROR web1 14:19:05  'leanengine.request': <Request 'http://api.leancloud.cn/1.1/functions/species' [POST]>,
ERROR web1 14:19:05  'werkzeug.request': <Request 'http://api.leancloud.cn/1.1/functions/species' [POST]>,
ERROR web1 14:19:05  'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7f885d16a1e0>,
ERROR web1 14:19:05  'wsgi.input': <gevent.pywsgi.Input object at 0x7f88538c4530>,
ERROR web1 14:19:05  'wsgi.multiprocess': False,
ERROR web1 14:19:05  'wsgi.multithread': False,
ERROR web1 14:19:05  'wsgi.run_once': False,
ERROR web1 14:19:05  'wsgi.url_scheme': 'http',
ERROR web1 14:19:05  'wsgi.version': (1, 0)} failed with LeanCloudError
ERROR web1 14:19:05

本次测试的性能统计截图:

那其实是云引擎没有正确 catch LeanCloudError,导致客户端解析不到合法的结果,就崩溃了。

一个简单的做法是在云引擎代码里做这样的处理:

try:
    do your thing
except LeanCloudError as e:
    if e.code == 429:
        do 429 workaround
    else:
        raise e

但还是推荐检查一下云引擎的代码,看看是不是可以优化查询。云引擎访问 API 是没有网络延迟的,如果同时发起多个查询很容易就超限了。

您的意思是问题可能发生在云引擎端对吗?就是被调用的云函数如果在服务器端发起多个数据库的查询操作也会导致这一情况?

@engine.define
def geneId(**params):
    print 'geneId is called!!!!!'
    if engine.current_user != None: # 只有已登录用户才有权限调用该云函数
        objId = params["id"]
        queryForGene = leancloud.Query('Gene')
        try:
            gene = queryForGene.get(objId)                       # 标记点1
        except leancloud.LeanCloudError as e:
            print '查询gene出错,id = ', objId
            raise e                                              # 标记点2
        else:
            #查询成功
            #有pointer的需要特殊处理,只记录pointer的id
            oldStatDict = gene.get(key_Gene_stat)

            newStatDict = {}
            if oldStatDict is not None:
                newStatDict = statDictPointerToIdString(oldStatDict)

            geneDataDict = {
                key_Gene_name_zh_Hans:gene.get(key_Gene_name_zh_Hans),
                key_Gene_dominant:gene.get(key_Gene_dominant),
                key_Gene_color:gene.get(key_Gene_color),
                key_Gene_shape:gene.get(key_Gene_shape),
            }

            if len(newStatDict) > 0:
                geneDataDict[key_Gene_stat] = newStatDict
        
            jsonString = json.dumps(geneDataDict, separators = (',',':'))
            return jsonString

就是以上云函数 geneId 被调用多次。但我仔细看了一遍代码,该云函数只进行了一次查询(标记点1),调用的statDictPointerToIdString函数里只是对字典里的poiner类型都处理为string(否则用json.dumps会出错),没有查询操作。当出现Error429时,行号是指向的标记点2。

按我的粗浅理解,我的那个调用频率和代码是不会造成线程不够用的,问题有可能是当调用这个云函数时,还有其他大量的云函数或者查询调用请求我没注意到,只是运行到这行因为显式raise了才误以为问题是发生在这里。这样理解对吗?

云函数的请求实际上是通过 Python SDK 封装的 REST API,所以只要遇到异常都会抛出来的。

之前我贴那一大段Error信息时说当有这些时没有Error429,这一点我说错了,因为在那一大段Error中间我也发现了这句 LeanCloudError: LeanCloudError: [429] Too many requests.

您之前说建议我检查云引擎代码,我检查过了(见贴的云函数代码那条回复),没有发现该函数使用大量查询请求。另外我也在客户端检查了,发生问题的时刻可以理解为瞬间进行了以下操作:

  • 6次geneId云函数调用
  • 1次云函数B调用 云函数B构造与geneId基本一致但更简单。
  • 20次使用UnitySDK默认接口通过id查询某个表A的该条数据
  • 4次使用UnitySDK默认接口通过id查询某个表B的该条数据

这两条云函数的开始都进行了engine.current_user != None的判断,除了该current_user语句外各自只进行了一次通过id查询的操作(例上个回复中代码的标记点A处)。我的判断是问题不出在这些云函数的代码中。

之后我没有什么头绪了,请问该如何定位问题呢? 谢谢!

我的建议是如果没办法快速定位问题的话可以先忽略 429 错误,在抛出 LeanCloudError 的时候做一下异常处理,如果愿意的话可以同时在这里打印一条日志以便日后 debug。

出现超限的工作线程数只能说明有超限,但并不一定能 100% 反映实际情况。因为和工作线程数正相关的指标是 QPS,而 QPS 的数量又和服务端处理请求的时间相关。我们丢弃一个请求所需的时间非常小,这样客户端很快就可以再次发起另一个请求,可能会引起非常大的 QPS。也就是说你在图表里看到的这么猛的超限很可能只会在开发阶段出现。

所以我的建议如前面所说,如果没发现代码有大的问题,就建议先绕过这里。如果这个问题频繁复现,再考虑如何解决。解决方案一般都是优化查询。

我对异常理解的不是很好“在抛出 LeanCloudError 的时候做一下异常处理”我这样做您看是否可以:
在我贴出的那段代码中的以下片段:

        try:
            gene = queryForGene.get(objId)                       # 标记点1
        except leancloud.LeanCloudError as e:
            print '查询gene出错,id = ', objId
            raise e                                              # 标记点2
        else:
            #查询成功
            do somthing

将except这段的raise e这行删除。客户端查询失败后有重新发起查询的机制。而在服务器端,我也不清楚except得到该异常后我能做什么处理,所以“catch”后不作任何动作,后面的代码就不执行了,等客户端过些毫秒再重新发起查询。

另外我没明白“我们丢弃一个请求所需的时间非常小,这样客户端很快就可以再次发起另一个请求,可能会引起非常大的 QPS。”这句话中,客户端很快再次发起另一个请求,这是指的例如我在客户端写的只要未成功就重新请求的类,还是unity SDK中有类似的不成功则重试发起请求的机制?

建议判断一下 LeanCloudErrorcode,根据不同情况分别处理。譬如当 e.code == 429 时打印「并发超限」。

错误代码可以参考 我们的文档2,不过一般来说只要处理几种比较常见的情况就可以了。

非常感谢!

我的代码现在没有对任何异常做任何处理,都是只再次raise了。

那么我就先尝试catch到429只打印,其他的才raise,然后看看是否还有这个问题。

另外我一个用户在一瞬间发起20多个普通查询和10次左右的云函数调用,这个不算过份吧?

更新:

我尝试了将项目里所有的except句中的raise e改为以下,该问题仍然存在。

except leancloud.LeanCloudError as e:
    print '查询Species出错,id = ', objId
    if e.code == 429:
        print '并发超限, code = 429'
    else:
        raise e

云引擎网页后台中的出错信息:

ERROR web1 17:21:34 Traceback (most recent call last):
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/gevent/pywsgi.py", line 884, in handle_one_response
ERROR web1 17:21:34     self.run_application()
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/geventwebsocket/handler.py", line 88, in run_application
ERROR web1 17:21:34     return super(WebSocketHandler, self).run_application()
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/gevent/pywsgi.py", line 870, in run_application
ERROR web1 17:21:34     self.result = self.application(self.environ, self.start_response)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/__init__.py", line 52, in __call__
ERROR web1 17:21:34     return self.cloud_app(environ, start_response)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/werkzeug/local.py", line 228, in application
ERROR web1 17:21:34     return ClosingIterator(app(environ, start_response), self.cleanup)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/cors.py", line 51, in __call__
ERROR web1 17:21:34     return self.app(environ, cors_start_response)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/authorization.py", line 43, in __call__
ERROR web1 17:21:34     return self.app(environ, start_response)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/leanengine.py", line 73, in __call__
ERROR web1 17:21:34     self.process_session(environ)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/engine/leanengine.py", line 84, in process_session
ERROR web1 17:21:34     user = leancloud.User.become(session_token)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/user.py", line 59, in become
ERROR web1 17:21:34     response = client.get('/users/me', params={'session_token': session_token})
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/client.py", line 90, in new_func
ERROR web1 17:21:34     return func(headers=headers, *args, **kwargs)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/client.py", line 171, in new_func
ERROR web1 17:21:34     response = func(*args, **kwargs)
ERROR web1 17:21:34   File "/usr/local/lib/python2.7/dist-packages/leancloud/client.py", line 163, in new_func
ERROR web1 17:21:34     raise leancloud.LeanCloudError(content.get('code', 1), content.get('error', 'Unknown Error'))
ERROR web1 17:21:34 LeanCloudError: LeanCloudError: [429] Too many requests.
ERROR web1 17:21:34 {'CONTENT_LENGTH': '33',
ERROR web1 17:21:34  'CONTENT_TYPE': 'application/json',
ERROR web1 17:21:34  'GATEWAY_INTERFACE': 'CGI/1.1',
ERROR web1 17:21:34  'HTTP_ACCEPT': '*/*',
ERROR web1 17:21:34  'HTTP_CONNECTION': 'close',
ERROR web1 17:21:34  'HTTP_HOST': 'api.leancloud.cn',
ERROR web1 17:21:34  'HTTP_USER_AGENT': 'UnityPlayer/5.5.0f3 (http://unity3d.com)',
ERROR web1 17:21:34  'HTTP_X_FORWARDED_FOR': '::ffff:10.10.225.216',
ERROR web1 17:21:34  'HTTP_X_FORWARDED_PORT': '80',
ERROR web1 17:21:34  'HTTP_X_FORWARDED_PROTO': 'https',
ERROR web1 17:21:34  'HTTP_X_FORWARDED_PROTOCOL': 'http',
ERROR web1 17:21:34  'HTTP_X_LC_CLIENT_VERSION': 'net-portable-2.0.0.0',
ERROR web1 17:21:34  'HTTP_X_LC_ID': 'GSIRUlB5taiAdvjI1jynXHu7-gzGzoHsz',
ERROR web1 17:21:34  'HTTP_X_LC_INSTALLATION_ID': 'ce91dfe8-3c0d-455f-bae1-354fab2fe3ad',
ERROR web1 17:21:34  'HTTP_X_LC_KEY': 'zPc1Cs5CKAr2i0QGPxtHhOma',
ERROR web1 17:21:34  'HTTP_X_LC_SESSION': '28l6z4jr28u2b8lo84f74nide',
ERROR web1 17:21:34  'HTTP_X_REAL_IP': '125.33.93.118',
ERROR web1 17:21:34  'HTTP_X_UNITY_VERSION': '5.5.0f3',
ERROR web1 17:21:34  'PATH_INFO': '/1.1/functions/geneId',
ERROR web1 17:21:34  'QUERY_STRING': '',
ERROR web1 17:21:34  'REMOTE_ADDR': '10.10.58.223',
ERROR web1 17:21:34  'REMOTE_PORT': '40748',
ERROR web1 17:21:34  'REQUEST_METHOD': 'POST',
ERROR web1 17:21:34  'SCRIPT_NAME': '',
ERROR web1 17:21:34  'SERVER_NAME': '3e196bed2a81',
ERROR web1 17:21:34  'SERVER_PORT': '3000',
ERROR web1 17:21:34  'SERVER_PROTOCOL': 'HTTP/1.1',
ERROR web1 17:21:34  'SERVER_SOFTWARE': 'gevent/1.1 Python/2.7',
ERROR web1 17:21:34  '_app_params': {'id': u'GSIRUlB5taiAdvjI1jynXHu7-gzGzoHsz',
ERROR web1 17:21:34                  'key': u'zPc1Cs5CKAr2i0QGPxtHhOma',
ERROR web1 17:21:34                  'master_key': None,
ERROR web1 17:21:34                  'session_token': u'28l6z4jr28u2b8lo84f74nide'},
ERROR web1 17:21:34  'leanengine.request': <Request 'http://api.leancloud.cn/1.1/functions/geneId' [POST]>,
ERROR web1 17:21:34  'werkzeug.request': <Request 'http://api.leancloud.cn/1.1/functions/geneId' [POST]>,
ERROR web1 17:21:34  'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7f05898e91e0>,
ERROR web1 17:21:34  'wsgi.input': <gevent.pywsgi.Input object at 0x7f0584ea8188>,
ERROR web1 17:21:34  'wsgi.multiprocess': False,
ERROR web1 17:21:34  'wsgi.multithread': False,
ERROR web1 17:21:34  'wsgi.run_once': False,
ERROR web1 17:21:34  'wsgi.url_scheme': 'http',
ERROR web1 17:21:34  'wsgi.version': (1, 0)} failed with LeanCloudError
ERROR web1 17:21:34

3张后台统计数据截图:

app ID: GSIRUlB5taiAdvjI1jynXHu7-gzGzoHsz
麻烦请您帮忙看一下,谢谢!

另外我一个用户在一瞬间发起20多个普通查询和10次左右的云函数调用,这个不算过份吧?

如果这三十多个请求都是并发发送的话,确实会给服务器造成压力,达到工作线程上限,并且对客户端(手机)造成一定性能瓶颈。现代浏览器中,如果对同一个 Host 的 URL 并发请求的话,浏览器也会做并发限制,将并发请求改为串行的。

具体到这个问题,我建议把这三十多个请求,改成串行调用。或者考虑到速度,改为最大两个/三个的并发调用。这样就不再很容易的达到并发上限了。


关于不能捕获所有 429 异常的问题,根据日志发现现在的 SDK 在执行实际的云函数代码之前,还会根据在 SDK 当前登录的用户,获取一次当前用户信息。这个时候抛出的 429 异常并没有被捕获,导致请求挂掉。

目前此问题已经记录在 GitHub: https://github.com/leancloud/python-sdk/issues/2951 ,我们尽快解决,请查看此 issue 来跟踪问题解决状态。

1 人赞了这个帖子.

非常感谢!
关于此issue中“在执行实际的云函数代码之前,还会根据在 SDK 当前登录的用户,获取一次当前用户信息”描述的情况,是否就是描述的我代码中在云引擎最初的if engine.current_user != None:

我想办法在客户端重构成例如每帧最多调用2/3次查询,然后再测试反馈。

问一下,如果某个瞬间的并发请求占满了线程,那是会丢弃请求,还是排队处理呢?

会丢弃请求,这样客户端可以自行处理重试逻辑

另外如果是游戏里的「每帧」都会发起请求,那肯定是有问题的……